In short: Security headers are instructions from the web server to the browser that protect visitors from attacks such as cross-site scripting, clickjacking and unencrypted connections. The most important are HSTS, Content Security Policy, X-Content-Type-Options, protection against foreign frames, Referrer-Policy and Permissions-Policy. They are sensible hardening but do not replace updates on the server.
What security headers achieve and what they do not
With every page request, the web server sends headers along with the content. Some of them control security functions of the browser. This can be used to specify, for example, that a page is loaded only over HTTPS or that the browser executes no scripts from foreign domains.
The classification is important for operators: headers protect mainly the visitors, for example against session theft or manipulated forms. They do not close vulnerabilities on the server. A weakness in PHP, in the CMS or in an extension remains, no matter how well the headers are set. In combination with clean code, however, they considerably limit the consequences of many mistakes.
The most important security headers at a glance
| Header | Purpose | Recommendation as a starting point |
|---|---|---|
| Strict-Transport-Security (HSTS) | forces HTTPS for future visits | max-age=31536000; includeSubDomains |
| Content-Security-Policy (CSP) | defines permitted sources for scripts, images, frames | individual, first as report-only |
| X-Content-Type-Options | stops the browser from guessing file types | nosniff |
| X-Frame-Options or CSP frame-ancestors | prevents embedding in foreign pages (clickjacking) | SAMEORIGIN or frame-ancestors 'self' |
| Referrer-Policy | limits which address is passed on to other sites | strict-origin-when-cross-origin |
| Permissions-Policy | switches off browser features such as camera or location | allow only needed features |
You should set HSTS only once all subdomains are reliably reachable over HTTPS. The browser remembers the instruction for the specified time; a mistake then cannot be withdrawn immediately. If necessary, start with a short duration and increase it after successful tests. The additional option preload enters the domain permanently into browser vendors' lists and should be set only after careful review.
Headers are also not the only place where the browser offers protection. Cookies should carry the attributes Secure, HttpOnly and SameSite so that sessions cannot be intercepted over unencrypted connections or by scripts. How to set this up in PHP is described in the guide on sessions.
Setting security headers in Nginx
In Nginx, the headers belong in the server block of the respective website. The addition always ensures that they are also sent on error pages.
server {
# ...
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" always;
server_tokens off;
}
A peculiarity of Nginx: if a location block sets its own add_header directives, the headers from the server block are not inherited there. Therefore also check subpages and static files. Further hardening for this combination is described in Securing Nginx with PHP-FPM.
Setting security headers in Apache
On Apache you need the module mod_headers. The directives belong in the VirtualHost or, if you have no access to it, in the .htaccess.
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
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=()"
Header always set Content-Security-Policy-Report-Only "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
</IfModule>
ServerTokens Prod
ServerSignature Off
ServerTokens and ServerSignature belong in the main configuration. They prevent the server from stating its exact version in responses and error pages. In php.ini you achieve the same for PHP with expose_php = Off.
Introducing the Content Security Policy step by step
The Content Security Policy is the most effective but also the most demanding header. It defines from which sources scripts, stylesheets, images, fonts and frames may be loaded. If a planted script is loaded from a foreign domain, the browser blocks it.
Modern websites often embed analytics tools, payment providers, map services or videos. A policy that is too strict then disables functions. So proceed in stages:
- Start with
Content-Security-Policy-Report-Onlyfirst. The browser reports violations in the developer console or to a collection address but blocks nothing. - Record all needed sources and approve them specifically instead of using blanket wildcards.
- Gradually move inline scripts into files or give them nonces, so that
'unsafe-inline'can be dispensed with. - Switch to
Content-Security-Policyonly after a test phase without unexpected reports.
The CSP complements clean escaping in the code but does not replace it. How to secure output in PHP properly is in the guide Preventing cross-site scripting in PHP.
Checking headers and avoiding typical mistakes
After every change, you should check that the headers actually arrive. This works without additional tools using curl:
# Test the configuration and reload
nginx -t && systemctl reload nginx
# Show the headers of the home page and of a static file
curl -sI https://www.example.de/ | grep -iE 'strict|content-security|x-content|x-frame|referrer|permissions|server'
curl -sI https://www.example.de/favicon.ico | grep -iE 'strict|x-content'
Typical mistakes are headers set twice when the web server and the application send the same headers, an upstream CDN that removes or overwrites headers, and headers that appear only on the home page but not on redirects or error pages. Also check after updates of the CMS or of security extensions whether the headers have changed.
So define one place where the headers are maintained, ideally the web server or the upstream service. Many CMS extensions also offer settings for security headers. That is convenient but easily leads to duplicate or contradictory entries if the server also sets headers. For individual tests, the browser's developer tools are also suitable: in the network tab you see the response headers for each request, and the console shows violations of the Content Security Policy.
How does PHP SOS help?
- We check the current headers of all your websites, including behind CDN and proxy, and point out gaps and contradictions.
- We set the headers centrally in the web server as part of server hardening and test the home page, subpages and error pages.
- We introduce the Content Security Policy in report-only mode, evaluate the reports and switch it to enforcement after agreement.
- Where code changes are not immediately possible, we complement the protection with WAF rules and virtual patching.
Frequently asked questions
What are security headers?
Security headers are additional information in the web server's response. They tell the browser, for example, to load the page only over HTTPS, not to execute foreign scripts, or not to display the page inside foreign frames. They protect mainly the visitors.
Do security headers protect against PHP2Shell?
No. PHP2Shell is a vulnerability in PHP on the server, while headers work in the visitor's browser. The update to a fixed PHP version and the restart of all workers help against PHP2Shell. Headers are supplementary hardening against other types of attack.
Can a Content Security Policy break my website?
Yes, a policy that is too strict blocks needed scripts, fonts or embedded content. That is why it is introduced first in report-only mode, the reports are evaluated, and only then is it switched to enforcement.
Is X-XSS-Protection still useful?
No. Current browsers no longer evaluate this header, and the recommendation is to omit it or set it to 0. Protection against cross-site scripting comes from clean escaping in the code and a Content Security Policy.