Security headers occupy an unusual place in web application defense: they fix nothing in the code itself, yet they determine how much damage a flaw can do once it exists. A cross-site scripting bug that would otherwise execute freely gets blocked at the browser. A referrer leak that would otherwise expose a session token never leaves the page. The vulnerability still exists somewhere in the code, but the header decides whether it ever becomes an incident.
This workshop covers hardening headers at the code level: HTML and application middleware, the layer most developers can act on directly without waiting for infrastructure access. Meta tag examples appear wherever a header genuinely supports one. Where no meta equivalent exists, that limitation is stated outright rather than glossed over. A closing section explains why server and reverse-proxy configuration remains the proper long-term home for these settings.
1. Content-Security-Policy (CSP)
What is CSP?
The Content-Security-Policy (CSP) header controls which resources (scripts, styles, images) browsers are allowed to load, protecting your website from XSS and data injection attacks.
Problem: Unsafe or Missing CSP
- No
base-uriDirective: - Missing
base-uriallows attackers to inject<base>tags, redirecting relative URLs to malicious domains. - Overly Permissive
script-src: - If
script-srcpermits unsafe sources, attackers can execute malicious scripts. - Use of
unsafe-inlineorunsafe-eval: - Allows inline JavaScript or eval-based code execution, enabling XSS.
Solution
Set a strict CSP to limit resource loading and execution.
HTML Implementation:
Add a <meta> tag to your HTML <head> section:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://trusted-cdn.example.com;
object-src 'none';
base-uri 'self';">
This meta tag version carries real limitations worth knowing before relying on it. It cannot include report-uri, frame-ancestors, or sandbox, and it has no Report-Only mode. It also cannot safely hold a nonce, since a nonce embedded in a static HTML file never changes and its purpose collapses as a result. Nonces only function when generated fresh with every request, which requires the server or application layer shown next.
What It Does:
default-src'self': Restricts default resource loading to the same domain.script-src'self' https://trusted-cdn.example.com: Allows scripts from your domain and trusted CDNs.object-src'none': Blocks plugins like Flash or Java.base-uri'self': Prevents injectedtags from altering URLs.
Nginx Configuration:
set $csp "default-src 'self'; ";
set $csp "${csp}script-src 'self' https://trusted-cdn.example.com 'nonce-random123'; ";
set $csp "${csp}object-src 'none'; base-uri 'self';";
add_header Content-Security-Policy $csp;
Node.js (Express):
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
const csp = [
"default-src 'self';",
`script-src 'self' https://trusted-cdn.example.com 'nonce-${nonce}';`,
"object-src 'none';",
"base-uri 'self';"
].join(" ");
res.setHeader("Content-Security-Policy", csp);
next();
});
2. X-Content-Type-Options
What is X-Content-Type-Options?
This header ensures browsers respect the declared Content-Type of files, blocking MIME-type sniffing.
Problem: Missing X-Content-Type-Options
Without it, a browser may disregard the declared type and treat an uploaded image or text file as something executable, turning an otherwise harmless upload into a route toward cross-site scripting or phishing.
Solution
Set the X-Content-Type-Options header to nosniff.
HTML Implementation:
None exists. This header has no meta tag form and must be set through the server or the application itself.
Nginx Configuration:
add_header X-Content-Type-Options "nosniff";
Node.js (Express):
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
next();
});
3. Referrer-Policy
What is Referrer-Policy?
This header governs how much information about the referring page a browser shares with outside sites, keeping sensitive details such as session identifiers from leaking through the URL itself.
Problem: Missing Referrer-Policy
Without this header, browsers send complete URLs of referring pages to external sites, including sensitive data such as session tokens or unique identifiers.
Solution
Set the Referrer-Policy header to no-referrer.
HTML Implementation:
Add a <meta> tag to your HTML <head> section:
<meta name="referrer" content="no-referrer">
Nginx Configuration:
add_header Referrer-Policy "no-referrer";
Node.js (Express):
app.use((req, res, next) => {
res.setHeader("Referrer-Policy", "no-referrer");
next();
});
4. X-XSS-Protection (Deprecated)
What is X-XSS-Protection?
This header once triggered a browser's built-in XSS filter, blocking pages that showed signs of a reflected cross-site scripting attempt.
A Word on Deprecation
That filter no longer exists in any meaningful form. Chrome removed its XSS Auditor entirely, Firefox never implemented the header in the first place, and Edge retired its own filter years ago. OWASP and MDN both list this header as deprecated and warn that under certain conditions it can introduce the very vulnerability it was meant to prevent.
Historical Context
Older browsers without CSP support once depended on this header as their only line of defense against reflected XSS. That browser landscape no longer describes any meaningful share of real users.
Solution:
This header should not be relied upon for protection. If a framework or server sends it automatically, it should be set explicitly to 0, disabling the legacy filter behavior and leaving CSP as the actual line of defense.
HTML Implementation:
None exists. This header has no meta tag equivalent.
Nginx Configuration:
add_header X-XSS-Protection "0";
Node.js (Express):
app.use((req, res, next) => {
res.setHeader("X-XSS-Protection", "0");
next();
});
Combining Headers in HTML
Only CSP and Referrer-Policy have a meta tag form, so a combined static HTML block looks like this:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://trusted-cdn.example.com;
object-src 'none';
base-uri 'self';">
<meta name="referrer" content="no-referrer">
X-Content-Type-Options and X-XSS-Protection cannot join this block. Both require the header-based configuration shown earlier.
Testing Security Headers
Once these headers are live, confirming they actually reach the browser matters as much as setting them. SecurityHeaders.com inspects live HTTP response headers directly. Alternatively, opening Browser DevTools with F12, moving to the Network tab, and reading the response headers directly confirms the same thing without a third party involved.
Beyond Code: Server-Level Hardening
Meta tags and application middleware protect HTML documents only. A JSON response, an image, or a downloaded file never passes through an HTML head, so none of that protection reaches them. Setting these headers at the server or reverse-proxy layer, through Nginx, Apache, or a CDN's edge configuration, guarantees every response leaving the infrastructure carries them, independent of whether any single application route remembered to set them.
Conclusion
Security headers guard against cross-site scripting, MIME confusion, and referrer leakage, though the layer at which they are set changes how reliable that protection actually is. Code-level hardening through HTML meta tags and middleware offers a solid starting point and remains the fastest way to patch what sits directly under a team's control. Full coverage across every type of response still depends on enforcing these same headers at the server itself.
It is worth setting realistic expectations for how these gaps typically surface. In most automated security scans and formal pentest reports, missing or misconfigured versions of the three headers discussed here, CSP, X-Content-Type-Options, and Referrer-Policy, tend to appear as low-risk or purely informational findings rather than critical ones. That classification does not make them safe to leave unaddressed. Scanners flag them precisely because each one removes a layer of friction an attacker can lean on once a more serious flaw, an injection point or a broken access control, is already present in the system. Reviewing these findings ahead of an audit remains one of the simplest ways to shorten a report and keep a reviewer's attention fixed on the issues that genuinely demand deeper remediation.
Sources
- OWASP Cheat Sheet Series, Content Security Policy — cheatsheetseries.owasp.org
- MDN Web Docs, Content-Security-Policy: frame-ancestors directive — developer.mozilla.org
- W3C, Content Security Policy Level 3 — w3.org
- Google web.dev, Mitigate XSS with a strict CSP — web.dev
- Cloudflare Workers Docs, Set security headers — developers.cloudflare.com
- Cloudflare Turnstile Docs, Content Security Policy — developers.cloudflare.com
- MDN Web Docs, X-Content-Type-Options header — developer.mozilla.org
- MDN Web Docs, <meta http-equiv> HTML attribute — developer.mozilla.org
- MDN Web Docs, Referrer-Policy header — developer.mozilla.org
- MDN Web Docs, <meta name="referrer"> HTML attribute value — developer.mozilla.org
- Google web.dev, Referer and Referrer-Policy best practices — web.dev
- OWASP, Secure Headers Project — owasp.org
- OWASP Cheat Sheet Series, HTTP Headers Cheat Sheet — cheatsheetseries.owasp.org
- MDN Web Docs, X-XSS-Protection header — developer.mozilla.org
- OWASP, Deprecate X-XSS-Protection, Issue #13 — github.com
- OWASP ZAP Documentation, Missing Security Headers — zaproxy.org