What HTTP Security Headers Do and Why They Matter
Web applications face constant threats from attackers looking to exploit vulnerabilities in how browsers interpret and execute content. Even when application code is written carefully, browsers still make assumptions about content that can be manipulated. HTTP security headers give you a way to tell browsers exactly how to handle your content, reducing the risk of certain attacks succeeding.
These headers work at the web server level, applying to every response your application sends. They do not replace secure coding practices, but they create a valuable layer of defence that makes exploitation significantly harder. For a PHP application running on Nginx or Apache, configuration happens in the web server, which means one set of rules protects everything the application serves.
How Browser Security Headers Work
When a browser receives a response from your server, it checks for specific headers that tell it how to behave. A header like X-Frame-Options tells the browser whether the page is allowed to appear inside an iframe. Content-Security-Policy tells it which scripts are allowed to run. Strict-Transport-Security tells it to refuse any plain HTTP connections.
Each header addresses a different attack vector. Together, they form a defence-in-depth strategy that protects against several common exploitation techniques. The headers covered in this guide are the ones that matter most for production web applications, and each one is worth understanding before you add it to your configuration.
X-Frame-Options: Stopping Clickjacking Attacks
Clickjacking works by embedding your page inside an invisible iframe on a malicious website. The attacker overlays transparent buttons on top of your content, tricking visitors into clicking something they never intended. A user might think they are clicking a button on your site, but they are actually triggering an action on their own logged-in session.
X-Frame-Options controls whether your page can be displayed inside a frame at all. The header accepts three values:
- DENY: The page cannot be displayed in any frame, including on the same origin. Use this for pages that should never be embedded, such as admin panels or account settings.
- SAMEORIGIN: The page can be displayed in a frame, but only when the parent frame comes from the same origin. This is useful for pages that legitimately use framing on your own site.
- ALLOW-FROM: This was intended to allow specific trusted domains, but browser support is poor and the directive is effectively deprecated. Avoid relying on it.
For most applications, SAMEORIGIN is a sensible default. DENY is appropriate for high-risk pages like login forms, password change pages, and admin interfaces where framing serves no legitimate purpose.
Configuring X-Frame-Options in Nginx
add_header X-Frame-Options "SAMEORIGIN" always;
Configuring X-Frame-Options in Apache
Header always set X-Frame-Options "SAMEORIGIN"
The always parameter in Nginx ensures the header is included in error responses, not just normal pages. Apache's always modifier serves the same purpose.
Content-Security-Policy: Controlling What the Browser Loads
CSP is the most powerful security header available. It tells the browser exactly which resources can load and execute on a page, including scripts, stylesheets, images, fonts, and embedded frames. When configured correctly, CSP makes cross-site scripting attacks far more difficult because it blocks the execution of inline scripts and prevents resources from loading from untrusted domains.
A CSP works by defining directives for each resource type. The browser reads the policy and refuses to load anything that violates it. This means you need to understand what your application actually loads before you can write a restrictive policy that will not break things.
Understanding CSP Directives
A CSP header can contain many directives. The most important ones for most applications are:
- default-src: Sets the fallback for resource types that do not have their own directive. Start here when building a policy.
- script-src: Controls where JavaScript can load from. Blocking
'self'and'unsafe-inline'together is what makes CSP effective against XSS. - style-src: Controls CSS loading. Many frameworks use inline styles, which requires
'unsafe-inline'even though it weakens the policy. - img-src: Controls image sources. Including
data:allows inline images encoded as data URIs. - frame-ancestors: Controls where the page can be embedded. Setting this to
'none'achieves the same result as X-Frame-Options DENY. - base-uri: Restricts what can be set as the base URL for relative links. Prevents attackers from injecting a base tag that hijacks relative URLs.
- form-action: Controls where forms can submit. Prevents forms from submitting to untrusted domains.
A Basic CSP for PHP Applications
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
This policy assumes your application loads everything from the same origin. It allows inline styles because many CSS frameworks and content management systems generate styles that way. It blocks inline scripts, which is the most important XSS protection CSP provides.
Deploying CSP Safely with Report-Only Mode
Before enforcing a restrictive CSP, switch to report-only mode to identify violations without blocking anything. This lets you see what the browser would have blocked, so you can adjust the policy before it causes problems for users.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
You need a backend endpoint to receive violation reports. A simple PHP script can log these to a file for review. Once you have identified all legitimate uses of inline scripts, external resources, and other patterns, you can switch from Content-Security-Policy-Report-Only to Content-Security-Policy.
Note: If your application uses third-party JavaScript such as analytics tools, chat widgets, or advertising scripts, you need to explicitly allow those domains in your CSP. Each third-party service may require specific directives or host permissions. Check the documentation for each service before adding them to a restrictive policy.
Strict-Transport-Security: Forcing HTTPS Connections
HSTS tells browsers to refuse any plain HTTP connection to your domain for a specified period. This prevents man-in-the-middle attacks that work by intercepting the initial HTTP request before the redirect to HTTPS happens. Without HSTS, an attacker on the network can intercept that first request, serve a modified page, and capture sensitive information.
When HSTS is active, the browser handles the redirect itself before sending any request, which means the attacker's interception point never receives an unencrypted request.
HSTS Configuration Options
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- max-age: How long the browser should remember to only use HTTPS, measured in seconds. One year is
31536000. - includeSubDomains: Extends the policy to all subdomains. Use this only when every subdomain genuinely serves HTTPS.
- preload: Submits the domain to the HSTS preload list hardcoded into browsers. Once added, browsers will refuse HTTP even on first visit. This cannot be undone without careful planning.
Before enabling the preload flag, verify that every subdomain serves valid HTTPS. Adding a domain to the preload list and then discovering an HTTP-only subdomain breaks that subdomain for all visitors who have ever accessed the main domain.
If you are setting up HTTPS for the first time and want to understand common configuration issues, a practical review of your SSL setup can help identify problems before they affect visitors.
Referrer-Policy: Managing Information Leakage
When a visitor navigates from your site to an external link, the browser sends the URL of the page they left in the Referer header. This can expose more information than intended, including session tokens in query strings, internal page paths, and search terms.
Referrer-Policy controls exactly what information the browser sends. Several values are available, ranging from sending the full URL to sending nothing at all.
Referrer-Policy: strict-origin-when-cross-origin
This value sends the full URL for same-origin navigation, sends only the origin (not the path) for cross-origin navigation, and sends nothing when moving from HTTPS to HTTP. It is a practical default that protects path leakage while preserving useful analytics for internal navigation.
Other useful values include same-origin, which only sends the referrer for same-origin requests, and no-referrer, which never sends the referrer at all.
X-Content-Type-Options: Stopping MIME Sniffing
Browsers have historically tried to be helpful by examining the content of a file to determine its type, rather than trusting what the server says. This behaviour, called MIME sniffing, can be exploited when an attacker uploads a file that looks like HTML but is served with a safe content type. The browser sees the content, decides it is HTML, and executes any scripts it contains.
X-Content-Type-Options tells the browser to trust the Content-Type header and stop sniffing. It only accepts one value:
X-Content-Type-Options: nosniff
This header should be set on all responses for every application. It has no downsides for most setups and prevents a specific class of attack that exploits browser behaviour rather than application code.
Permissions-Policy: Disabling Unused Browser Features
Modern browsers offer many APIs that web applications can access, including camera, microphone, geolocation, and payment APIs. If your application does not use these features, there is no reason to leave them enabled. A compromised page could abuse these APIs to capture audio, track location, or access payment information.
Permissions-Policy (formerly called Feature-Policy) lets you disable APIs that your application does not need:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
The empty parentheses indicate that no origin is allowed to use these features. The page and any embedded iframes cannot access them. Add or remove features based on what your application actually uses.
X-XSS-Protection: Legacy Browser Filtering
X-XSS-Protection was designed to enable the browser's built-in cross-site scripting filter. However, this filter has itself been a source of vulnerabilities, and modern browsers have removed or plan to remove it. Chrome removed the XSS auditor entirely, and other browsers have followed.
If you want to set this header for compatibility with older browsers:
X-XSS-Protection: 1; mode=block
The mode=block parameter tells the browser to block the entire page rather than trying to sanitise the script. However, this header should not be relied upon for XSS protection in modern browsers. Content-Security-Policy is the correct mechanism for preventing cross-site scripting, and understanding how output encoding prevents XSS in PHP applications is worth reviewing alongside this.
Combining All Headers in Nginx
Once you have decided on the right configuration for each header, you can combine them into a single block in your Nginx configuration. Using the always parameter ensures headers are included in error responses, not just successful ones.
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
Place this block inside your server block, or better yet, inside an included file that you can reuse across virtual hosts. This makes it easier to maintain consistent security headers across multiple sites.
Testing Your Security Headers
After deploying security headers, validate that they are set correctly and do not break your application. Several tools can scan your headers and report on their presence and configuration.
Security Headers (securityheaders.com) provides a grading service that checks for common headers and assigns a rating based on how well your site is configured. Run your production URL through it and address any headers it flags as missing or misconfigured.
Test CSP specifically in report-only mode first and review the violation reports for several days. Common issues include:
- Inline scripts and styles from your application or content management system
- External resources that need explicit permission in img-src, font-src, or style-src
- Third-party JavaScript from analytics, chat, or advertising services that require additional domain allowances
- Content management system features that inject inline styles or scripts automatically
If you use Apache instead of Nginx, the same principles apply, though the configuration syntax differs. An Apache security configuration guide covers how to set these headers correctly for that web server.
Warning: Always test security header changes in a staging environment before applying them to production. Incorrect CSP configuration can break functionality without providing any error messages. Use report-only mode to catch problems before they affect real users.
Common Mistakes to Avoid
Several mistakes appear frequently when security headers are added to web servers. Avoiding these saves time and prevents unexpected problems.
- Enforcing CSP before testing: Deploying a restrictive Content-Security-Policy without first running in report-only mode will almost certainly break something. Test thoroughly first.
- Missing the always parameter in Nginx: Without it, headers are not set on error pages, leaving a gap in your protection.
- Setting overly permissive CSP values: Using
*as a source defeats the purpose of CSP. Be as restrictive as your application allows. - Adding HSTS preload without verifying HTTPS everywhere: Subdomains that serve HTTP will break for returning visitors whose browsers remember the HSTS policy.
- Relying on X-XSS-Protection as primary XSS protection: Modern browsers do not support this reliably. CSP is the correct mechanism for XSS prevention.
What Security Headers Cannot Do
Security headers are a valuable layer of defence, but they are not a complete security solution. They do not protect against server-side vulnerabilities, database injection, authentication bypass, or many other attack vectors. They also do not fix insecure application code.
The headers covered here address specific browser-level attack techniques. They work alongside secure coding practices, regular updates, proper access controls, and other security measures to form a complete picture. Think of them as one part of a broader security approach rather than a standalone fix.
When to Review Your Security Headers
Security headers should be reviewed when you make significant changes to your application, add new third-party services, or change how resources are loaded. A new analytics tool, advertising script, or embedded widget may require updates to your CSP.
Regular reviews also catch drift where configuration changes over time. Running your site through a security headers scanner every few months helps ensure nothing has been accidentally removed or misconfigured.