Skip to content

Cookie settings

Choose which kinds of cookies you allow. You can change your choice at any time via Cookie settings at the bottom of every page. Privacy statement

Necessary

Always on

Needed to log in, keep your cart and remember your choices. These are always on.

Which cookies?
  • weboke_sessie, weboke_klant, weboke_remember: your session and staying logged in
  • weboke_basket: your cart
  • wkBtwIncl: showing prices with or without VAT
  • wok_cookies: your cookie choice, 12 months

Measures which ad leads to a visit or order, so we do not pay for ads that do nothing.

Which cookies?
  • Google Ads via Google Tag Manager (_gcl_au, _gcl_aw), up to 90 days
  • OpenAI Ads, ads in ChatGPT
  • wok_aff: which partner referred you to us (affiliate programme), 60 days

4.6

Country and language

Each country has its own site, with prices and VAT for that country.

View

Module 7 · 7.4

English

Hardening your website: capabilities, headers and security plugins

Module 7: Security · Reading time: 10 minutes

Learning objective: After this lesson, you can harden a WordPress website with permissions, settings and security headers, and judge security plugins for what they're really worth.

Key concepts: hardening, file permissions (644, 755), DISALLOW_FILE_EDIT, no PHP in uploads, directory listing, hiding the version, X-Powered-By, security headers, backup files and .git in the web directory, WAF on the server, WAF in a plugin, malware scanner

What hardening is

Hardening means: setting up your website so that a mistake or vulnerability can do less damage. It doesn't replace updates and strong passwords (lesson 7.2 and 7.3), but it limits the consequences. Think of a house: updates are the locks, hardening is making sure that a burglar who does get in still can't reach the safe.

File permissions

Every file and directory on the server has permissions: who may read, write and execute it. They're written as three digits, for the owner, the group and everyone else. The guideline from the WordPress documentation:

WhatPermissionsMeaning
Directories755Owner may do anything, others may read and open
Files644Owner may read and write, others may only read
wp-config.phpstricter, for example 600 or 640Only the owner (and possibly the web server group) may read

Which value works for wp-config.php depends on how your host runs PHP: on most shared hosting, PHP runs as your own user, in which case 600 is enough. Try it and check whether the website still works.

Never 777. That means: everyone on the server may write to it. A guide that says "set the directory to 777 if uploading doesn't work" is solving the problem the wrong way. With good hosting, this is never necessary either.

Settings in wp-config.php

A few lines that make a big difference (above "stop editing"):

// Don't allow editing files from the dashboard (Appearance > Theme Editor and Plugins > Plugin Editor).
define( 'DISALLOW_FILE_EDIT', true );

// Never show error messages on visitors' screens (lesson 5.4).
define( 'WP_DEBUG', false );

With DISALLOW_FILE_EDIT, the editor that lets you change PHP files of themes and plugins from the dashboard disappears. An intruder who takes over an administrator account can then no longer put malicious code into your theme with two clicks. You can still install plugins; if you also want to forbid that (for example on a website you manage for a client), there's DISALLOW_FILE_MODS, but then updates via the dashboard stop working too.

No PHP in uploads, no directory listing

In wp-content/uploads you should only find images and documents, never PHP files. Intruders who manage to upload a file through a vulnerability specifically try to place a PHP file there and then request it. Block this with a .htaccess file in the uploads directory (on LiteSpeed and Apache):

<Files *.php>
Require all denied
</Files>

Also turn off directory listing. Without an index.php or index.html in a directory, some web servers show a list of all files ("Index of /wp-content/uploads/2026/11"). That's a shop window for anyone looking for backups or sensitive documents. In the .htaccess file of your web directory:

Options -Indexes

Many hosts have already turned this off by default. The exercise for this lesson checks it.

No leftovers in the web directory

Everything in the web directory can be requested via the web, except what the server executes as PHP or blocks. Dangerous are:

  • copies of wp-config.php (wp-config.php.bak, wp-config.old, wp-config.php~ from a text editor): these contain your database password and get sent as plain text (lesson 1.3);
  • backups such as backup.zip, site.tar.gz or database.sql in the web directory;
  • a .git directory left by a developer, which lets someone download your entire codebase (and sometimes passwords);
  • debug.log in wp-content (lesson 5.4);
  • old installation or test files such as info.php with phpinfo().

Keep backups outside the web directory or off the server, and clean up any leftovers.

Hiding the version: a small win

WordPress adds this to the HTML by default:

<meta name="generator" content="WordPress 7.1.4" />

and the same in the RSS feed. PHP sometimes announces itself in a header too: X-Powered-By: PHP/8.3.14. Hiding this is reasonable, but don't expect too much from it: intruders' programs just try a vulnerability regardless of the version you show, and versions can often also be deduced from ?ver= after scripts. Hiding it is tidying up, not security. A website that hides its version but doesn't update is just as vulnerable.

Here's how, in your own small plugin or in the functions.php of your child theme:

remove_action( 'wp_head', 'wp_generator' );
add_filter( 'the_generator', '__return_empty_string' );

And the PHP header with expose_php = Off in your host's PHP settings (in DirectAdmin under Select PHP version, Options), or with Header unset X-Powered-By in .htaccess.

Security headers

With a few headers, you tell the browser how to treat your website. The most important ones:

HeaderWhat it does
Strict-Transport-SecurityOnly ever via https (lesson 3.1)
X-Content-Type-Options: nosniffThe browser won't guess what type of file something is; prevents an uploaded "image" from being executed as a script
X-Frame-Options: SAMEORIGIN or Content-Security-Policy: frame-ancestors 'self'Other websites may not show your website in a frame; prevents clickjacking (an invisible button from your website hidden under someone else's)
Referrer-Policy: strict-origin-when-cross-originWhen someone clicks through to another website, only your domain name is sent along, not the full address
Permissions-PolicyTurns off browser features your website doesn't use (camera, microphone, location)
Content-Security-PolicyDetermines exactly which sources scripts and styles may come from. Powerful, but tricky to get right with WordPress and plugins; start with Report-Only mode

In .htaccess (on LiteSpeed and Apache):

<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

Afterwards, test whether everything still works: embedded content, payment buttons, a chat widget. External tools such as securityheaders.com and internet.nl show you what your website sends.

Security plugins, a fair look

There are many security plugins (for example Wordfence, Solid Security, All-In-One Security, Sucuri). They often bundle: a firewall, a malware scanner, login protection, two-factor authentication, notifications and a hardening checklist. Useful, but with limits:

  • A firewall in a plugin (WAF) only starts once PHP is running. A malicious request first has to pass through the web server and PHP before the plugin sees it. A vulnerability in a PHP file that's called directly, or an attack that heavily loads PHP itself, isn't always stopped by it. A firewall on the server (for example ModSecurity, Imunify360 or a firewall from your host) sits earlier in the chain and stops more, without your plugin doing the computing work. Ask your host what's running on the server.
  • A scanner finds things after the fact. A malware scanner tells you something's wrong. That's valuable, but it doesn't prevent anything. And a scanner on the same server as a hacked website can itself have been altered by the intruder.
  • They don't replace updates. A plugin with a known vulnerability stays vulnerable, even with a security plugin alongside it. Some security services offer "virtual patches" that block known attacks until you've updated; handy as a stopgap, not as a replacement.
  • They're a plugin themselves. More code, more to keep updated, sometimes a vulnerability of their own. And two security plugins at once often clash.
  • They cost processing power, especially scanners that run on your own server.

A sensible approach: first check what your host already does (firewall, malware scanning, blocking login attempts). Add what's missing, usually two-factor authentication and limiting login attempts. An extensive security plugin is mainly useful with hosting that does little itself.

Summary

  • Hardening limits the damage from a mistake or vulnerability; it doesn't replace updates and passwords.
  • Directories 755, files 644, wp-config.php stricter; never 777.
  • Turn on DISALLOW_FILE_EDIT, block PHP in uploads, turn off directory listing and clear backups, .git and debug.log out of the web directory.
  • Hiding the version is tidying up, not security; security headers help the browser protect your visitors.
  • A firewall on the server stops more than a plugin; security plugins are an addition, not a replacement.

Quiz

1. A guide says: "set wp-content to 777 if uploading doesn't work." What do you think of that?



2. What does DISALLOW_FILE_EDIT do?



3. Why does a firewall on the server sit earlier in the chain than a firewall in a plugin?



4. You've removed the generator tag. Is your website now safe from vulnerabilities in old versions?



5. Which header prevents another website from loading your website in an invisible frame?



Exercise

Exercise A7.4: What does your website show to everyone? Enter your website's address. The tool reads the homepage and the RSS feed (/feed/) and checks: is there no WordPress version in the generator tag of the HTML and the feed, does the X-Powered-By header not reveal a PHP version, and does your website send X-Content-Type-Options: nosniff, protection against frames (X-Frame-Options or frame-ancestors) and a Referrer-Policy? It's fine (orange) if HSTS (lesson 3.1) and Permissions-Policy are missing.

Exercise A7.4: What does your website show to everyone?


What you enter and what we find isn't stored. Your browser remembers your answers and whether you've passed the exercise. If you're signed in, we only save that you got the exercise (almost) right in My courses; you can delete that there too.

Exercise A7.4b: No leftovers in the web directory. Prove that it's your own website (TXT record or file with the exercise's code). The tool then requests a short fixed list: the directory /wp-content/uploads/ (no directory listing), /.git/HEAD, /wp-content/debug.log and a few common names for copies of wp-config.php. It's fine if none of these return content (404 or 403). The tool doesn't try anything else and doesn't change anything.

Exercise A7.4b: No leftovers in the web directory

This check requests paths that you should only have checked on your own website. First click "Show my code" and set that code as a TXT record on _weboke-cursus.<your domain>, or as a file on your website (see the explanation next to the code).


What you enter and what we find isn't stored. Your browser remembers your answers and whether you've passed the exercise. If you're signed in, we only save that you got the exercise (almost) right in My courses; you can delete that there too.

Further reading

These articles describe the steps in WebOké's panels; they work similarly in other panels.

One moment…

Set up your domains

For each domain, arrange the nameservers, the link to your hosting and who becomes the owner.

Loading your domains…