Improve wordpress security grade

Improve wordpress security grade

Upon setting up this blog from a fresh WordPress tarball, I decided to have some fun tuning the security standard on this blog.

TIPS: The same concept can be applied to all blogs (regardless of whether it is WordPress based or not), or websites.

Security Headers Check

I first head to Security Headers. It was graded at D+ at the beginning, failing 6 out of 7 security headers required. Archiving this was easy with the following chunk of nginx configs. Note that the Content Security Policy (CSP) config is a basic one which only makes the web security standard A BIT safer.

# Nginx config
location / {

  add_header X-XSS-Protection "1; mode=block";
  add_header X-Frame-Options "SAMEORIGIN";
  add_header X-Content-Type-Options nosniff;
  add_header Referrer-Policy strict-origin-when-cross-origin;
  add_header Feature-Policy "geolocation none;midi none;notifications *;push *;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;";
  add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' data: https:;object-src 'none';form-action 'self';base-uri 'self';frame-ancestors 'self';";
  add_header Strict-Transport-Security "max-age=31536100; includeSubDomains; preload;";

Providing these security headers alone will be sufficient to get it to grade A.

Report summary from

Next challenge

Further evaluation was done on Mozilla Observatory, which scored around 80 (grade B). Content Security Policy (CSP) configuration comes into play here, which after properly tuned, will make the score skyrocket!

The point of CSP is to define resource white-listing and the browser will honor the configuration. This improved security in the sense that we know these are all the resources loaded from this site. Anything else (perhaps injected or man-in-the-middle attack) will be blocked.

# Nginx response header config
Content-Security-Policy "script-src 'self';"

<script src=""/>

The header above tells the browser that Javascript loaded from (origin) is allowed to be executed.

But what about inline javascript? Vanilla wordpress installation contains some of these which need special care. I will use Google Analytic js snipplet for example:

  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'UA-0000000-1');

Putting this code will be blocked by default (CSP default) due to risky inline execution of Javascript. We can’t white-list such code by domain can we? No, in such situation we will use CSP resource hash

# Nginx response header config
Content-Security-Policy "script-src 'sha256-U5AMUkSG225njsKZtEE4LdDcsVcnW+ziDO6vWWbdWCY=';"

When the browser loads the script, it compares the computed hash against what’s defined in CSP headers. Execution of the script will be allowed IF the hash is identical, therefore reducing the risk of executing something malicious induced by an attack.

Chrome will show you the exact hash needed to configure the CSP block. Neat right?

In the end I have successfully eliminated the use of unsafe-inline in script-src, dramatically boosting the security grade. There’s still some usage of unsafe-inline in style-src section which won’t be able to be tackled using the CSP hash.

# Inline style (solvable with CSP hash)
body {
  color: red;

# Non-inline (I do not know what to call this). The only solution is to not define explicit style like this, but to define one in style.css
<p style="color: red;">sample</p>

I believe it can also be solved by CSP nonce, maybe in a later post. It should be done properly where backend dynamically generates a nonce hash and then returning the CSP header containing the generated nonce for browser validation, otherwise it’s pretty pointless to hardcode it.

Miscellaneous Fixes

Manually fix non-https links like

<link rel='dns-prefetch' href='' />
<link rel='dns-prefetch' href='' />

Manual removal of inline style like

- <li><a href="" class="tag-cloud-link tag-link-2 tag-link-position-2" style="font-size: 12px;">docker</a></li>
+ <li><a href="" class="tag-cloud-link tag-link-2 tag-link-position-2">docker</a></li>

Optional: Disabling emoji. Included these in functions.php in my child theme. Reason being this is not helpful and cost me extra work to deal with these in CSP.

remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );


  • Indeterminable CSP hash for Google fonts API due to the endpoint returning a different result based on the browser, so we can’t have one distinct hash for this! Refer to issue:
  • Host bypass mitigation with CSP strict-dynamic… I will let this slip for now due to time constraint.
Google CSP evaluator result

Final CSP Config

location / {
  add_header Content-Security-Policy "default-src 'none';script-src 'unsafe-inline' 'sha256-U5AMUkSG225njsKZtEE4LdDcsVcnW+ziDO6vWWbdWCY=';style-src 'self' 'unsafe-inline';style-src-elem 'self' 'unsafe-inline';img-src 'self' data:;font-src 'self' data:;object-src 'none';form-action 'self';base-uri 'self';frame-ancestors 'self';report-uri;";
Grade A+ from
History of grade improvements

That’s it! CSP does give me quite a headache, but I do see the point of doing this in order to make the site much safer.

One Reply to “Improve wordpress security grade”

Leave a Reply

Your email address will not be published. Required fields are marked *