WordPress Security Guide


Dev Cham

July 22, 2019

The One Champion

Keep Your Software Up To Date


The most common culprit of a hacked WordPress website is due to an outdated component. Outdated plugins, themes, and core open the portal for a potentially hacked site. When left un-updated, these outdated files are traceable and make your site a target by outside intruders.

Step 1

Ensuring your WordPress site is up-to-date is simple. When you see an orange notification in your WordPress dashboard next to plugins, themes, or a notification to upgrade WordPress, update ASAP!

If your site is hosted with WP Engine, we’ll automatically run these WordPress core updates for you, although you will need to be attentive with themes and plugins to update them accordingly to protect your website from malware.

If you’d rather not do it manually, you can configure automatic updates. To auto-upgrade WordPress core, insert this code into your wp-config.php file:

define( ‘WP_AUTO_UPDATE_CORE’, true );


Run the following command to update  plugins:

add_filter( ‘auto_update_plugin’, ‘__return_true’ );


Run the following command to themes, 

add_filter( ‘auto_update_theme’, ‘__return_true’ );


2.2 General Security Setup

Nonce your forms and urls

A nonce is a “number used once” to help protect URLs and forms from certain types of misuse, malicious or otherwise. WordPress nonces aren’t numbers, but are a hash made up of numbers and letters. Nor are they used only once, but have a limited “lifetime” after which they expire. During that time period the same nonce will be generated for a given user in a given context. The nonce for that action will remain the same for that user until that nonce life cycle has completed. For more information


Change Security Key in wp-config file.

Disable XML-RPC (If You Aren’t Using It)

Open .htaccess file paste this code.

# Block WordPress xmlrpc.php requests
<Files xmlrpc.php>
order deny,allow
deny from all
allow from

Disable PHP Error Reporting

Disable PHP Error Reporting

Some tutorials suggests putting the following code at the top of wp-config.php file disables PHP errors:


But unfortunately this does not work, however, because when that file calls wp-settings.php at the bottom, the error reporting will be overridden by WordPress’s own settings.

In order for the above line to work, it must come after the call to wp-settings.php or at the end of wp-config.php file, like below…


/** Sets up WordPress vars and included files. */
require_once(ABSPATH . ‘wp-settings.php’);



  1. Disable PHP Execution


  1. Remove the WordPress version from the theme

Some sites will recommend that you open your header.php file and get rid of this code:

<meta name=”generator” content=”WordPress <?php bloginfo(‘version’); ?>” />


Or others will recommend that you open your functions.php and add the following function:

remove_action(‘wp_head’, ‘wp_generator’);


2.3 Securing File Structure


  1. Set all folder permission to 755 and files to 644. See section below.
  2. Make sure the wp-config.php file is not accessible by others.
  3. Prevent direct access to your filesif(!define(‘ABSPATH’)){define(‘ABSPATH’, dirname(__FILE__).’/’);


  4. Remove or block via .htaccess files license.txt, wp-config-sample.php, and readme.html.
  5. Disable file edit via wp-config.php by adding the following code: define(‘DISALLOW_FILE_EDIT’,true);
  6. Prevent directory listing via .htaccess by adding the following code: Options All -Indexes
  7. Password protect the folder wp-admin (unblock only the needed files)


2.3.1 File Permissions

Enable/Disable Directory Listing

To have the web server produce a list of files for such directories, use the below line in your .htaccess.

Options +Indexes

To have an error (403) returned instead, use this line.

Options -Indexes

More – http://www.clockwatchers.com/htaccess_dir.html Why Not 640 or 750 permissions. Why 644, 755 

644 means that files are readable and writable by the owner of the file and readable by users in the group owner of that file and readable by everyone else.

755 is the same thing, it just has the execute bit set for everyone. The execute bit is needed to be able to change into the directory. (CD command) This is why directories are commonly set to 755.

Regular HTML files need to be viewable by the Apache user (user nobody on cPanel servers). Since this user is typically not in the group of the ownership of the file (and if it were, and in a shared hosting environment every user would have to be in this group, which kind of defeats the purpose of limiting to 640 or 750) the world section of the permissions needs to be set to readable.

Now in a suPHP environment, PHP files can just as easily be set to 600. This is because the PHP files are read by the web server as the username specified in the virtualhost section in Apache. In a non-suPHP environment though, PHP files are still read by the apache user and therefore would require a world-readable bit. Again, this would only apply to PHP parsed files, not regular .html or .htm files.

Most scripts have separate config files which include login information. And yes, for those files I would recommend that they are set to a permission setting of 600 to prevent others from reading it. Other PHP files could also be set to 600, but you’re really not saving yourself anything if the PHP files have no critical information included. For example, setting the permissions to WordPress’s main index.php file to 600 kind of defeats the point because someone can just download WordPress from WordPress’s site and read the index.php file.

suPHP and PHP as CGI really are not a standard. PHP developers cannot recommend to set the permissions on the files to 600 because if PHP is running as a DSO module on the server, then using 600 permissions will not work. This is one reason why I think suPHP and PHP as CGI should be standard on any shared hosting server, but the owner of that server or the owner of the account on that server needs to realize that it is important to set the permissions on these config files to 600 and ignore the recommendations in the software’s specifications.

Whenever a new files get created, should get created with same owner, group and permission in that directory.

Make a Directory World Writable 

in order to make a directory writable by the webserver we have to set the directory’s owner or group to Apache’s owner or group and enable the write permission for it. Usually, we set the directory to belong to the Apache group (apache or www-data or whatever user is used to launch the child processes) and enable the write permission for the group.


chgrp apache /path/to/mydir
chmod g+w /path/to/mydir

but path to mydir should be secure. You can make it 777 as well but above option is better. 


More – http://www.g-loaded.eu/2008/12/09/making-a-directory-writable-by-the-webserver/


2.4 Securing Admin Access


  1. Remove login links from the theme. If you would like to try fix this yourself, the code for this will probably be located in header.php, just do a search for “log in” and remove/comment out the markup/function that places it there.


  1. Rename/Change the login page url. Use this plugin “Lockdown WP Admin”.


  1. Change or omit the admin username. By default, WordPress gives the primary domain account the username “admin”. Leaving the username as “admin” is an instant security threat to your site. If an attacker wants to crack the code, half of the puzzle is already solved and all that’s left to guess is your password.


Removing or changing the “admin” username is the next step to improving site security. To do this, simply go to the “users” section of the WordPress admin panel and rename or delete the “admin” account or username.

WP Engine does not allow the use of the “admin” username and will automatically remove it for you, replacing the admin name with a “wpengine account” name. This account is used by our support team. We implement special configurations to prevent attacks on the “wpengine” user account specifically.


2.5 Securing User Access


  1. Login Attempts – Limit attempts to Login, Verify OTP, Resend OTP and generate OTP APIs for a particular user. Have an exponential backoff set or/and something like a captcha based challenge.


  1. Login Error Messages – Make the login error message more generic like 
    1. Please enter valid information to login.
    2. Please check your email if your account is registered, or register for a new account.


  1. Password Reset – Check for randomness of reset password token in the emailed link, and set an expiration on the reset password token for a reasonable period. Expire the reset token after it has been successfully used.


  1. Email Verification – Edit email/phone number feature should be accompanied by a verification email to the owner of the account.


2.6 Securing Database

  1. Change the default table prefix.
  2. Schedule weekly backup of the database (Backup WP, WP DB Backup etc. )
  3. Use a strong password containing uppercase, lowercase, numbers, and special characters for the database user (use password generator)


2.7 Security Headers and Configurations


  1. Add CSP header to mitigate XSS and data injection attacks. This is important.
  2. Add CSRF header to prevent cross site request forgery. Also add SameSite attributes on cookies.
  3. Add HSTS header to prevent SSL stripping attack.
  4. Add your domain to the HSTS Preload List
  5. Add X-Frame-Options to protect against Clickjacking.
  6. Add X-XSS-Protection header to mitigate XSS attacks.
  7. Update DNS records to add SPF record to mitigate spam and phishing attacks.
  8. Add subresource integrity checks if loading your JavaScript libraries from a third party CDN. For extra security, add the require-sri-for CSP-directive so you don’t load resources that don’t have an SRI sat.
  9. Use random CSRF tokens and expose business logic APIs as HTTP POST requests. Do not expose CSRF tokens over HTTP for example in an initial request upgrade phase.
  10. Do not use critical data or tokens in GET request parameters. Exposure of server logs or a machine/stack processing them would expose user data in turn.


2.8 Same Origin Policy

Same-origin policy is an important concept in the web application security model. Under the policy, a web browser permits scripts contained in a first web page to access data in a second web page, but only if both web pages have the same origin. An origin is defined as a combination of URI scheme, host-name, and port number.


Compared URL Outcome Reason
http://www.example.com/dir/page2.html Success Same protocol, host and port
http://www.example.com/dir2/other.html Success Same protocol, host and port
http://username:password@www.example.com/dir2/other.html Success Same protocol, host and port
http://www.example.com:81/dir/other.html Failure Same protocol and host but different port
https://www.example.com/dir/other.html Failure Different protocol
http://en.example.com/dir/other.html Failure Different host
http://example.com/dir/other.html Failure Different host (exact match required)
http://v2.www.example.com/dir/other.html Failure Different host (exact match required)
http://www.example.com:80/dir/other.html Depends Port explicit. Depends on implementation in browser.

Link to check headers of website:



Enable mod_headers on server:
a2enmod headers
service apache2 restart

You can check via phpinfo(); that it’s enabled or not.


Write code in .htaccess / httpd.conf:

<IfModule mod_headers.c>
Header set X-Frame-Options “SAMEORIGIN”


Few other syntaxes :

  1. Set X-Frame-Options ALLOW-FROM https://www.example.com 
  2. Always unset X-Frame-Options. This is only useful if you’re looking to disable X-Frame-Options completely – which in some cases is not always the correct solution. Allowing Optimizely to function requires managing X-Frame-Options which also means constantly updating it. All websites should be using X-Frame-Options to increase their website security for their visitors. 

<IfModule mod_headers.c>
Header set X-Frame-Options “DENY”
# `mod_headers` cannot match based on the content-type, however,
# the `X-Frame-Options` response header should be send only for
# HTML documents and not for the other resources.

<FilesMatch “\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$”>
Header unset X-Frame-Options

<IfModule mod_headers.c>

Header set Access-Control-Allow-Origin “*”

Allow access Control from all domain for all request. You can try with ajax call also.


  1. Configuring nginx
    add_header X-Frame-Options SAMEORIGIN;

Way to interact with other server if same origin policy is blocked



<script type=”text/javascript” src=”http://example.com/?some-variable=some-data&jsonp=parseResponse”></script>

The dynamically generated JavaScript from example.com may look like: parseResponse({“variable”: “value”, “variable2”: “value2”})


  1. Use below library


How to check that browser support CORS
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if (“withCredentials” in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest !=”undefined”){
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
return xhr;

var request = createCORSRequest(“get”,””);

if (request){
request.onload = function() {


request.onreadystatechange = handler;


smartlybuilt-facebook-blog smartlybuilt-linkedin-blog smartlybuilt-twitter-blog

Similar Posts