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 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 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 to 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’ );

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 been completed. For more information


Change Security Key in wp-config file.

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

Open the .htaccess file paste this code.

# Block WordPress xmlrpc.php requests

order deny, allow
deny from all
allow from

Disable PHP Error Reporting

Disable PHP Error Reporting

Some tutorials suggest putting the following code at the top of the 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 the 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:


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

remove_action(‘wp_head’, ‘wp_generator’);

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)

File Permissions

Enable/Disable Directory Listing

To have the webserver 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 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 webserver as the username specified in the virtual host 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 kinds 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 setting 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 new files get created, should get created with the 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 the above option is better. 

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

Securing Admin Access

  1. Remove login links from the theme. If you would like to try to 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.

Securing User Access

  1. Login Attempts – Limit attempts to log in, Verify OTP, Resend OTP, and generate OTP APIs for a particular user. Has 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 log in.
    2. Please check your email if your account is registered, or register for a new account.
  1. Password Reset – Check for the randomness of the 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 – The edit email/phone number feature should be accompanied by a verification email to the owner of the account.

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)

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 records 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 getting request parameters. Exposure of server logs or a machine/stack processing them would expose user data in turn.

Same Origin Policy

The 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 URLOutcome Reason
http://www.example.com/dir/page2.htmlSuccess Same protocol, host and port
http://www.example.com/dir2/other.htmlSuccess Same protocol, host and port
http://username:password@www.example.com/dir2/other.htmlSuccess Same protocol, host and port
http://www.example.com:81/dir/other.htmlFailure Same protocol and host but different port
https://www.example.com/dir/other.htmlFailure Different protocol
http://en.example.com/dir/other.htmlFailure Different host
http://example.com/dir/other.htmlFailure Different host (exact match required)
http://v2.www.example.com/dir/other.htmlFailure Different host (exact match required)
http://www.example.com:80/dir/other.htmlDepends Port explicit. Depends on implementation in browser.

Link to check headers of the website:


Enable mod_headers on server:
a2enmod headers
service apache2 restart

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

Write code in .htaccess / httpd.conf:

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. 

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

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

Allow access Control from all domains for all requests. You can try with ajax call also.

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

Way to interact with another server if same-origin policy is blocked


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


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

Similar Posts