WordPress security from Cloudflare

As we mentioned in the previous post “WordPress security in three levels“, today we are going to talk about how to secure a little more our WordPress installation, besides that we can also make it faster with the use of its CDN and other configurations.

It is totally beyond the scope of this article to cover all the options offered by Cloudflare, as it could be a complete book or a course. We could include Web3 options, applications using its API, workers, Stream or Zero Trust to configure our applications and private networks without using VPN. A whole world of possibilities that presents something new every day.

But let’s get down to what we are interested in, security.

Improve our certificate score

One of the first things I do when I take on a website is to check its certificate “score” from the SSL Labs service https://www.ssllabs.com/ssltest/index.html.

Here if you have less than an A+, you should make some improvements.

Grade B certificate

For that, inside our Cloudflare account and selecting the domain, we go to SSL/TLS -> General information and in the SSL/TLS encryption mode we select Full (strict). This option is the most secure, although obviously we must have a valid certificate at the origin. In reality, this configuration will not influence the final score, but it is the most appropriate and safe one.

Then, in Perimeter Certificates we select Always use HTTPS so that any request to the http protocol will be redirected to https.

We configure HTTP Strict Transport Security (HSTS) with a maximum age of 6 months, disable subdomains if we do not have websites installed on them and enable preloading. Before making these changes we must make sure for at least a few days that our website works perfectly under https or we may be left without access to it.

In the minimum TLS version we select TLS 1.2 and enable opportunistic encryption and TLS 1.3.

We also activate the automatic HTTPS rewrites which will change any request to an http resource to its https equivalent and which will also avoid the use of a plugin for this (which will also be done at a lower level such as the server or even from PHP, for example the well-known Really Simple SSL).

Although we have these rewrites activated at DNS level, we should also search in the database for resources of our domain with http and make the change for the correct ones with https. This is something very easy with WP CLI or with the following PHP script https://interconnectit.com/search-and-replace-for-wordpress-databases/

The next option to check Certificate Transparency Monitoring is completely optional, if we select it, we will receive an email every time a certificate is issued or renewed for our domain or subdomains.

When it comes to SSL/TLS we don’t have to change anything else. We can now recheck our certificate score.

Grade A Certificate

Page rules and headers

I’m going to skip the security tab which I’ll leave for the end and let’s now look at the Rules tab, starting with the page rules.

In the free account we can create up to three page rules. If you have a contact form in which you want to reduce SPAM, we can activate a rule with the address of the form in which we activate the integrity of the browser.

Rule contact

Another rule that I usually activate for most clients is the one that secures the login a little more, for this we create a new rule and in the URL we put *ourdomain.com/wp-login.php* to work in the version with and without www and even if the URL has parameters. In the settings we select again the Browser Integrity Check and enable it, then Security Level and set it to I’m Under Attack and finally Disable Performance.

Rule login

With these options we will avoid many attempts of automated attacks, bots, etc. and every time we go to connect to the administration panel we will see the Cloudflare browser integrity check screen.

Cloudflare browser checking

Now we go to the Transformation Rules section (10 available in the free version) and click on Managed Transformations where we check Remove “X-Powered-by” headers and Add security headers.

Managed security headers

Next select Create Transformation Rule and in the drop-down Modify Response Header.

Here we name our rule and we will see/create three:

  1. Rule name: Add Security headers
    • Expression: (http.request.uri contains “” and not http.request.full_uri contains “.css” and not http.request.full_uri contains “.js” and not http.request.uri contains “.jpg” and not http.request.uri contains “.gif” and not http.request.uri contains “.png” and not http.request.uri contains “.webp”)
    • And we add:
      • Set Static with access-control-allow-credentials header and value to true
      • Set Static with access-control-allow-headers header and value to *.
      • Set Static with header access-control-allow-methods and value to GET, POST, OPTIONS
      • Set Static with access-control-allow-origin header and value to *.
      • Set Static with permissions-policy header and value to accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=()
      • Set Static with referrer-policy header and value to strict-origin-when-cross-origin
      • Set Static with header x-permitted-cross-domain-policies and value to none
  2. Rule name: Remove link header
    • Expression: (http.request.uri contains “” and not http.request.full_uri contains “.css” and not http.request.full_uri contains “.js” and not http.request.uri contains “.jpg” and not http.request.uri contains “.gif” and not http.request.uri contains “.png” and not http.request.uri contains “.webp”)
    • And we add Remove and in the header name link
  3. Rule name: Rules Author
    • Expression: (http.request.uri contains “”), i.e. we apply it to all URLs
    • And we add:
      • Set Static with the x-cl-rules-author header and value to https://jclc.me/codeable.
Modify headers

With these actions we have configured actions such as adding XSS security headers:

  • X-Content-Type-Options: nosniff
  • X-XSS-Protection: 1; mode=block
  • X-Frame-Options: SAMEORIGIN
  • Referrer-Policy: same-origin
  • Expect-CT: max-age=86400, enforce

and removal of the headers identifying the X-Powered-by server.

In addition, in the transformation rules we have added some additional safety rules with the first rule. We have to be very clear about what we are doing and know how to solve it before failures, since we are limiting certain requests, which in some websites may be necessary. Or for example, the web may need the location we are limiting with permissions-policy and the value geolocation=(). We can adapt it with https://www.permissionspolicy.com/, but I repeat, we must always, ALWAYS, know what we are doing.

In the second rule we are eliminating several link headers that take us to the API and other services that in most cases are not necessary, although we use these services and the only thing they provide are new clues to attackers.

The third one as you can see does not add anything to the security and it is only a personalized header where you can add your name, link, etc. 😉

Security

And finally we go to the security tab. In the Configuration option we set the Security level to Medium and the Challenge Passage to 30 minutes. Enable browser integrity check. We have also enabled Privacy Pass support.

Next we are going to create some security rules, for what inside security we go to WAF (Web Application Firewall) and in Firewall rules we have the possibility of creating up to 5 in the free version, but we will see that we can group several in a single rule.

The first thing to consider is whether we use Redsys. If so, the first thing we do in WAF is to go to Tools and there we add the three IP ranges of Redsys (193.16.243.0/24, 194.224.159.0/24 and 195.76.9.0/24), with the action allow, by selecting This web site in Zone and putting in Notes Redsys IP range.

As we can see, the IP Access rules are executed before the firewall rules (and after the page rules).

IP access rules

Now we go to Firewall Rules and in the case of using Redsys, we are going to create the following rule:

Name: Allow Redsys ASN requests
Expression: (ip.geoip.asnum eq 31627)
Action: Skip -> Browser Integrity Check

We can use the expression generator for the simplest rules (default), or to copy and paste, edit expressions in text format.

Redsys ASN Rule

The next rule will be to block requests to the XML-RPC protocol that we saw in the previous WordPress Security article in three levels, for which we create the following rule:

Name: Block XML-RPC requests
Expression: (http.request.uri.path contains "/xmlrpc.php")
Action: Block

We create a new rule to block all attempts to access PHP files within the wp-content directory. No plugin or theme should execute code directly in that directory, if so, it is either poorly done or there must be a very specific and special reason. The rule would be:

Name: PHP access to wp-content
Expression: (http.request.uri.path contains "/wp-content/" and http.request.uri.path contains ".php")
Action: Block

If our web server is Litespeed, then their own cache plugin, runs a PHP file in the directory (something they should avoid), so we will create an exception for that file and change the expression to the following: (http.request.uri.path contains "/wp-content/" and http.request.uri.path contains ".php" and not http.request.uri.path contains "/wp-content/plugins/litespeed-cache/guest.vary.php")

As we can see, the procedure is simple and we can chain rules with and and or. But we can also group several rules together as long as the action is the same. For example, the two rules above have the Block action, so we can group them with the use of parentheses and the rule would look like this:

(http.request.uri.path contains "/xmlrpc.php") or
(http.request.uri.path contains "/wp-content/" and http.request.uri.path contains ".php" and not http.request.uri.path contains "/wp-content/plugins/litespeed-cache/guest.vary.php")

As we can see, this gives us a great power and we are going to see it with the following rule to avoid unwanted accesses:

Name: Bad access
Expression: (cf.threat_score gt 14) or
(cf.threat_score gt 10 and cf.client.bot) or
(http.request.uri.query contains "author_name=" and not http.request.uri.path contains "/wp-admin/") or
(http.request.uri.query contains "author=" and not http.request.uri.path contains "/wp-admin/") or
(http.request.uri contains "/wp-json/wp/v2/users/" and not http.referer contains "/wp-admin/") or
(http.request.uri contains "wp-config.") or (http.request.uri contains "setup-config.") or
(http.request.uri.path contains ".js.map") or
(lower(http.request.uri.path) contains "phpmyadmin") or
(lower(http.request.uri.path) contains "thinkphp") or
(http.request.uri.path contains "/phpunit") or
(raw.http.request.uri contains "../") or (raw.http.request.uri contains "..%2F") or
(http.request.uri contains "passwd") or
(http.request.uri contains "/dfs/") or
(http.request.uri contains "/autodiscover/") or
(http.request.uri contains "/wpad.") or
(http.request.uri contains "/wallet.dat") or
(http.request.uri contains "webconfig.txt") or
(http.request.uri contains "vuln.") or
(http.request.uri contains ".env") or (http.request.uri contains ".ini") or (http.request.uri contains ".log") or (http.request.uri contains ".sql") or
(http.request.uri.query contains "bin.com/") or (http.request.uri.query contains "bin.net/") or (raw.http.request.uri.query contains "?%00") or
(http.request.uri.query contains "eval(") or (http.request.uri.query contains "base64") or (http.request.uri.query contains "var_dump") or
(http.request.uri.query contains "<script") or (raw.http.request.uri.query contains "%3Cscript") or
(http.request.full_uri contains "<?php") or
(http.cookie contains "<?php") or
(http.cookie contains "<script") or (http.referer contains "%3Cscript") or
(http.cookie contains "base64") or (http.cookie contains "var_dump") or
(upper(http.request.uri.query) contains "$_GLOBALS[") or
(upper(http.request.uri.query) contains "$_REQUEST[") or
(upper(http.request.uri.query) contains "$_POST[")

Action: Block

In the above example we are preventing access to .sql files (database dumps with vital data), log files, configuration files, script execution attempts, bot accesses, etc.

We can also block all traffic from a specific country, from several countries ((ip.geoip.country eq "RU") or (ip.geoip.country eq "CN")) or from all countries, except those we mark in our rule ((not ip.geoip.country in {"ES" "PT"})) and thousands of other possibilities.

Another rule we can create is to check that the accesses to the comments file of the website have the referrer of our domain (http.request.uri.path contains "/wp-comments-post.php" and not http.referer contains "midominio.com")

We can also block access to our administration and only allow access to a certain IP (we must always remember to create the exception for /wp-admin/admin-ajax.php) or block it for everyone and whitelist our IP as we did with the Redsys IP ranges.

Another rule that we can create and combine with the previous ones:

To block SPAM in contact forms: (http.request.version in {"HTTP/1.0" "HTTP/1.1" "HTTP/1.2"} and http.request.uri eq "/contacto/" and not http.user_agent contains "Googlebot" and not http.user_agent contains "Bingbot" and not http.user_agent contains "DuckDuckBot" and not http.user_agent contains "facebot" and not http.user_agent contains "Slurp" and not http.user_agent contains "Alexa")

Block SPAM from contact forms and in comments: (http.request.uri contains "/wp-admin/admin-ajax.php" and http.request.method eq "POST" and not http.referer contains "tusitioweb.com") or (http.request.uri contains "/wp-comments-post.php" and http.request.method eq "POST" and not http.referer contains "tusitioweb.com").

As we have seen, we can generate multiple combinations, use IPs, IP ranges, countries, continents, cookies, URLs, referers, request methods, etc., convert to upper or lower case, see if it contains something, if it does not, if it is the same or different. In addition to being able to “schedulethemthrough cron” or perform other actions through the API as in the following example where I empty the cache.

Are the rules working?

It is very easy to check if the rules you have created are working, for example you can test the XML-RPC rule by visiting the URL of your website which should be blocked at https://tuweb.com/xmlrpc.php.

Or in the case of the rule that blocks different attempts to access your web https://tuweb.com/prueba/volcado.sql and in both cases you should see the Cloudflare Access Denied page, where it also indicates a Ray ID that helps us to search for the specific event.

Cloudflare access denied

Next in security we can go to general information, firewall events and see all the events:

Firewall events

There we can edit the columns and for example show the Ray ID, or even better, filter and show only the events that match our ID that we copied from the previous page:

Filtered by Ray ID

Here we can see that an attempt has been made to access a database dump file (.sql), the date, IP, path, the rule that acted, as well as other data of the access attempt (which in this case was a test of mine).

If we have many access attempts by attackers from an IP, just as we enable the IP for Redsys, we can deny a certain IP or range of IPs.

And now?

As you can see, and as I warned you at the beginning, we could write a whole book about the different Cloudflare options. We have not gone into CDN and web acceleration options.

Another very interesting option is Zaraz, from which for example we can send the Google Analytics script (among many others) to the web without inserting it from the server, or the different improvements of the CDN or the use of Early Hints to get a few extra seconds out of the optimization, or even hide parts of our website such as some texts from visitors of dubious reputation.

Zaraz
Zaraz injected script

But that’s another matter. If you want to have all the WAF rules that I have named here and the improvements that I am making on them, you can follow this repository where I have put them: https://github.com/CarlosLongarela/WordPress-Cloudflare-WAF-rules

I look forward to all your comments.

P.S.: First article: Is my website secure?

Second article: WordPress security in three levels.

Join my superlist ;)

I won't share your details with anyone or bombard you with emails, only when I publish a new post or when I have something interesting to share with you.

Leave a Comment