[{"data":1,"prerenderedAt":704},["ShallowReactive",2],{"blog-how-to/add-security-headers":3},{"id":4,"title":5,"body":6,"category":673,"date":674,"dateModified":674,"description":675,"draft":676,"extension":677,"faq":678,"featured":676,"headerVariant":690,"image":691,"keywords":691,"meta":692,"navigation":693,"ogDescription":694,"ogTitle":691,"path":695,"readTime":691,"schemaOrg":696,"schemaType":697,"seo":698,"sitemap":699,"stem":700,"tags":701,"twitterCard":702,"__hash__":703},"blog/blog/how-to/add-security-headers.md","How to Add Security Headers to Your Web App",{"type":7,"value":8,"toc":645},"minimark",[9,13,17,21,49,54,70,74,77,110,114,200,204,232,252,271,287,307,338,364,368,372,402,406,417,428,432,438,442,446,466,470,505,509,527,531,559,571,575,609,621],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-add-security-headers-to-your-web-app",[18,19,20],"p",{},"Protect your app with essential HTTP security headers",[22,23,24,27],"tldr",{},[18,25,26],{},"TL;DR (20 minutes)",[18,28,29,30,34,35,34,38,34,41,44,45,48],{},"Add security headers to protect against clickjacking, XSS, MIME sniffing, and protocol downgrade attacks. Essential headers: ",[31,32,33],"code",{},"X-Content-Type-Options: nosniff",", ",[31,36,37],{},"X-Frame-Options: DENY",[31,39,40],{},"Strict-Transport-Security",[31,42,43],{},"Referrer-Policy",", and ",[31,46,47],{},"Content-Security-Policy",". Configure via your web server, framework middleware, or hosting platform.",[50,51,53],"h2",{"id":52},"prerequisites","Prerequisites",[55,56,57,61,64,67],"ul",{},[58,59,60],"li",{},"Access to your web server configuration, framework code, or hosting platform dashboard",[58,62,63],{},"Your site served over HTTPS (required for HSTS)",[58,65,66],{},"Basic understanding of HTTP headers",[58,68,69],{},"A way to test headers (browser DevTools or online scanner)",[50,71,73],{"id":72},"why-security-headers-matter","Why Security Headers Matter",[18,75,76],{},"HTTP security headers instruct browsers how to handle your site's content. Without them, your app is vulnerable to:",[55,78,79,86,92,98,104],{},[58,80,81,85],{},[82,83,84],"strong",{},"Clickjacking"," - Attackers embed your site in an invisible iframe to hijack clicks",[58,87,88,91],{},[82,89,90],{},"MIME sniffing attacks"," - Browsers misinterpret file types, potentially executing malicious content",[58,93,94,97],{},[82,95,96],{},"XSS attacks"," - Malicious scripts run in your users' browsers stealing data",[58,99,100,103],{},[82,101,102],{},"Protocol downgrade"," - Attackers force HTTP connections to intercept traffic",[58,105,106,109],{},[82,107,108],{},"Information leakage"," - Sensitive URLs exposed via referrer headers",[50,111,113],{"id":112},"essential-security-headers-reference","Essential Security Headers Reference",[115,116,117,133],"table",{},[118,119,120],"thead",{},[121,122,123,127,130],"tr",{},[124,125,126],"th",{},"Header",[124,128,129],{},"Recommended Value",[124,131,132],{},"Protection",[134,135,136,148,159,169,179,189],"tbody",{},[121,137,138,142,145],{},[139,140,141],"td",{},"X-Content-Type-Options",[139,143,144],{},"nosniff",[139,146,147],{},"Prevents MIME type sniffing",[121,149,150,153,156],{},[139,151,152],{},"X-Frame-Options",[139,154,155],{},"DENY or SAMEORIGIN",[139,157,158],{},"Blocks clickjacking via iframes",[121,160,161,163,166],{},[139,162,40],{},[139,164,165],{},"max-age=31536000; includeSubDomains",[139,167,168],{},"Enforces HTTPS connections",[121,170,171,173,176],{},[139,172,43],{},[139,174,175],{},"strict-origin-when-cross-origin",[139,177,178],{},"Limits referrer information leakage",[121,180,181,183,186],{},[139,182,47],{},[139,184,185],{},"default-src 'self'; ...",[139,187,188],{},"Controls resource loading, prevents XSS",[121,190,191,194,197],{},[139,192,193],{},"Permissions-Policy",[139,195,196],{},"camera=(), microphone=(), geolocation=()",[139,198,199],{},"Restricts browser feature access",[50,201,203],{"id":202},"step-by-step-implementation","Step-by-Step Implementation",[205,206,208,213,216,226],"step",{"number":207},"1",[209,210,212],"h3",{"id":211},"expressjs-with-helmet","Express.js with Helmet",[18,214,215],{},"The easiest way to add security headers in Express is using the Helmet middleware:",[217,218,223],"pre",{"className":219,"code":221,"language":222},[220],"language-text","npm install helmet\n","text",[31,224,221],{"__ignoreMap":225},"",[217,227,230],{"className":228,"code":229,"language":222},[220],"const express = require('express');\nconst helmet = require('helmet');\n\nconst app = express();\n\n// Add all security headers with sensible defaults\napp.use(helmet());\n\n// Or configure individually:\napp.use(helmet({\n  contentSecurityPolicy: {\n    directives: {\n      defaultSrc: [\"'self'\"],\n      scriptSrc: [\"'self'\", \"'unsafe-inline'\"],\n      styleSrc: [\"'self'\", \"'unsafe-inline'\"],\n      imgSrc: [\"'self'\", \"data:\", \"https:\"],\n      connectSrc: [\"'self'\", \"https://api.example.com\"],\n      fontSrc: [\"'self'\", \"https://fonts.gstatic.com\"],\n      objectSrc: [\"'none'\"],\n      upgradeInsecureRequests: [],\n    },\n  },\n  hsts: {\n    maxAge: 31536000,\n    includeSubDomains: true,\n    preload: true,\n  },\n}));\n\napp.listen(3000);\n",[31,231,229],{"__ignoreMap":225},[205,233,235,239,246],{"number":234},"2",[209,236,238],{"id":237},"nextjs-configuration","Next.js Configuration",[18,240,241,242,245],{},"Add headers in ",[31,243,244],{},"next.config.js",":",[217,247,250],{"className":248,"code":249,"language":222},[220],"/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  async headers() {\n    return [\n      {\n        source: '/(.*)',\n        headers: [\n          {\n            key: 'X-Content-Type-Options',\n            value: 'nosniff',\n          },\n          {\n            key: 'X-Frame-Options',\n            value: 'DENY',\n          },\n          {\n            key: 'X-XSS-Protection',\n            value: '1; mode=block',\n          },\n          {\n            key: 'Referrer-Policy',\n            value: 'strict-origin-when-cross-origin',\n          },\n          {\n            key: 'Permissions-Policy',\n            value: 'camera=(), microphone=(), geolocation=()',\n          },\n          {\n            key: 'Strict-Transport-Security',\n            value: 'max-age=31536000; includeSubDomains',\n          },\n          {\n            key: 'Content-Security-Policy',\n            value: \"default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none';\",\n          },\n        ],\n      },\n    ];\n  },\n};\n\nmodule.exports = nextConfig;\n",[31,251,249],{"__ignoreMap":225},[205,253,255,259,265],{"number":254},"3",[209,256,258],{"id":257},"nextjs-middleware-dynamic-headers","Next.js Middleware (Dynamic Headers)",[18,260,261,262,245],{},"For dynamic CSP with nonces, use middleware in ",[31,263,264],{},"middleware.ts",[217,266,269],{"className":267,"code":268,"language":222},[220],"import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');\n\n  const cspHeader = `\n    default-src 'self';\n    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';\n    style-src 'self' 'nonce-${nonce}';\n    img-src 'self' blob: data: https:;\n    font-src 'self';\n    object-src 'none';\n    base-uri 'self';\n    form-action 'self';\n    frame-ancestors 'none';\n    upgrade-insecure-requests;\n  `.replace(/\\s{2,}/g, ' ').trim();\n\n  const response = NextResponse.next();\n\n  response.headers.set('Content-Security-Policy', cspHeader);\n  response.headers.set('X-Content-Type-Options', 'nosniff');\n  response.headers.set('X-Frame-Options', 'DENY');\n  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');\n  response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n  response.headers.set('X-Nonce', nonce);\n\n  return response;\n}\n\nexport const config = {\n  matcher: [\n    '/((?!api|_next/static|_next/image|favicon.ico).*)',\n  ],\n};\n",[31,270,268],{"__ignoreMap":225},[205,272,274,278,281],{"number":273},"4",[209,275,277],{"id":276},"nginx-configuration","nginx Configuration",[18,279,280],{},"Add to your nginx server block:",[217,282,285],{"className":283,"code":284,"language":222},[220],"server {\n    listen 443 ssl http2;\n    server_name example.com;\n\n    # Security Headers\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header X-Frame-Options \"DENY\" always;\n    add_header X-XSS-Protection \"1; mode=block\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n    add_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n    add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n    add_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none';\" always;\n\n    # ... rest of your config\n}\n",[31,286,284],{"__ignoreMap":225},[205,288,290,294,301],{"number":289},"5",[209,291,293],{"id":292},"apache-configuration","Apache Configuration",[18,295,296,297,300],{},"Add to your ",[31,298,299],{},".htaccess"," or virtual host config:",[217,302,305],{"className":303,"code":304,"language":222},[220],"\u003CIfModule mod_headers.c>\n    Header always set X-Content-Type-Options \"nosniff\"\n    Header always set X-Frame-Options \"DENY\"\n    Header always set X-XSS-Protection \"1; mode=block\"\n    Header always set Referrer-Policy \"strict-origin-when-cross-origin\"\n    Header always set Permissions-Policy \"camera=(), microphone=(), geolocation=()\"\n    Header always set Strict-Transport-Security \"max-age=31536000; includeSubDomains\"\n    Header always set Content-Security-Policy \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;\"\n\u003C/IfModule>\n",[31,306,304],{"__ignoreMap":225},[205,308,310,314,317,329,332],{"number":309},"6",[209,311,313],{"id":312},"cloudflare-configuration","Cloudflare Configuration",[18,315,316],{},"Add via Transform Rules in Cloudflare Dashboard:",[318,319,320,323,326],"ol",{},[58,321,322],{},"Go to Rules > Transform Rules > Modify Response Header",[58,324,325],{},"Create a new rule for all requests",[58,327,328],{},"Add each header as a \"Set static\" operation",[18,330,331],{},"Or use Cloudflare Workers:",[217,333,336],{"className":334,"code":335,"language":222},[220],"export default {\n  async fetch(request, env) {\n    const response = await fetch(request);\n    const newResponse = new Response(response.body, response);\n\n    newResponse.headers.set('X-Content-Type-Options', 'nosniff');\n    newResponse.headers.set('X-Frame-Options', 'DENY');\n    newResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');\n    newResponse.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n    newResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');\n    newResponse.headers.set('Content-Security-Policy', \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;\");\n\n    return newResponse;\n  },\n};\n",[31,337,335],{"__ignoreMap":225},[339,340,341,344],"warning-box",{},[18,342,343],{},"Security Checklist Before Deployment",[55,345,346,349,352,355,358,361],{},[58,347,348],{},"Ensure your site is fully served over HTTPS before enabling HSTS",[58,350,351],{},"Test Content-Security-Policy in report-only mode first",[58,353,354],{},"Verify X-Frame-Options doesn't break legitimate iframe embeds",[58,356,357],{},"Check that third-party scripts are allowed in your CSP",[58,359,360],{},"Start with a short HSTS max-age (e.g., 300) and increase gradually",[58,362,363],{},"Test on staging environment before production deployment",[50,365,367],{"id":366},"how-to-verify-it-worked","How to Verify It Worked",[209,369,371],{"id":370},"method-1-browser-devtools","Method 1: Browser DevTools",[318,373,374,377,380,387,390,393,399],{},[58,375,376],{},"Open your site in Chrome or Firefox",[58,378,379],{},"Press F12 to open DevTools",[58,381,382,383,386],{},"Go to the ",[82,384,385],{},"Network"," tab",[58,388,389],{},"Reload the page (Ctrl+R or Cmd+R)",[58,391,392],{},"Click on the first request (your HTML document)",[58,394,395,396],{},"Scroll down to ",[82,397,398],{},"Response Headers",[58,400,401],{},"Verify all your security headers are present",[209,403,405],{"id":404},"method-2-online-scanner","Method 2: Online Scanner",[18,407,408,409,416],{},"Use ",[410,411,415],"a",{"href":412,"rel":413},"https://securityheaders.com",[414],"nofollow","securityheaders.com"," to scan your site:",[318,418,419,422,425],{},[58,420,421],{},"Enter your URL and click Scan",[58,423,424],{},"Review your grade (aim for A or A+)",[58,426,427],{},"Check which headers are missing or misconfigured",[209,429,431],{"id":430},"method-3-command-line","Method 3: Command Line",[217,433,436],{"className":434,"code":435,"language":222},[220],"# Using curl to check headers\ncurl -I https://yoursite.com\n\n# Expected output includes:\n# x-content-type-options: nosniff\n# x-frame-options: DENY\n# strict-transport-security: max-age=31536000; includeSubDomains\n# referrer-policy: strict-origin-when-cross-origin\n# content-security-policy: default-src 'self'; ...\n",[31,437,435],{"__ignoreMap":225},[50,439,441],{"id":440},"common-errors-and-troubleshooting","Common Errors and Troubleshooting",[209,443,445],{"id":444},"headers-not-appearing","Headers not appearing",[55,447,448,454,460],{},[58,449,450,453],{},[82,451,452],{},"CDN caching",": Your CDN may be caching responses without headers. Purge the cache or add headers at the CDN level.",[58,455,456,459],{},[82,457,458],{},"Wrong config location",": Ensure headers are configured for all routes, not just specific paths.",[58,461,462,465],{},[82,463,464],{},"Server not restarted",": nginx and Apache require restart/reload after config changes.",[209,467,469],{"id":468},"csp-blocking-content","CSP blocking content",[55,471,472,482,492],{},[58,473,474,477,478,481],{},[82,475,476],{},"Inline scripts blocked",": Add ",[31,479,480],{},"'unsafe-inline'"," to script-src (less secure) or use nonces.",[58,483,484,487,488,491],{},[82,485,486],{},"Third-party resources blocked",": Add the domain to the appropriate directive (e.g., ",[31,489,490],{},"script-src 'self' https://cdn.example.com",").",[58,493,494,477,497,500,501,504],{},[82,495,496],{},"Images not loading",[31,498,499],{},"data:"," and ",[31,502,503],{},"https:"," to img-src for base64 and external images.",[209,506,508],{"id":507},"hsts-causing-issues","HSTS causing issues",[55,510,511,517],{},[58,512,513,516],{},[82,514,515],{},"Can't access HTTP version",": This is expected behavior. If you need to disable HSTS, set max-age=0 and wait for the original max-age to expire.",[58,518,519,522,523,526],{},[82,520,521],{},"Subdomain not HTTPS-ready",": Remove ",[31,524,525],{},"includeSubDomains"," until all subdomains support HTTPS.",[209,528,530],{"id":529},"x-frame-options-breaking-embeds","X-Frame-Options breaking embeds",[55,532,533,551],{},[58,534,535,538,539,542,543,546,547,550],{},[82,536,537],{},"Legitimate embeds blocked",": Use ",[31,540,541],{},"SAMEORIGIN"," instead of ",[31,544,545],{},"DENY",", or use CSP ",[31,548,549],{},"frame-ancestors"," for more control.",[58,552,553,538,556],{},[82,554,555],{},"Need to allow specific domains",[31,557,558],{},"Content-Security-Policy: frame-ancestors 'self' https://trusted.com",[560,561,562,565],"tip-box",{},[18,563,564],{},"Pro Tip",[18,566,408,567,570],{},[31,568,569],{},"Content-Security-Policy-Report-Only"," header first to test your CSP without breaking anything. Set up a reporting endpoint to collect violations and refine your policy before enforcing it.",[50,572,574],{"id":573},"frequently-asked-questions","Frequently Asked Questions",[576,577,578,585,591,597,603],"faq-section",{},[579,580,582],"faq-item",{"question":581},"What security headers should every website have?",[18,583,584],{},"Every website should have: X-Content-Type-Options: nosniff, X-Frame-Options: DENY or SAMEORIGIN, Strict-Transport-Security with max-age of at least 31536000, Referrer-Policy: strict-origin-when-cross-origin, and Content-Security-Policy. These protect against the most common web attacks.",[579,586,588],{"question":587},"Will adding security headers break my website?",[18,589,590],{},"Basic headers like X-Content-Type-Options, X-Frame-Options, and Referrer-Policy rarely cause issues. HSTS can cause problems if your site isn't fully HTTPS-ready. CSP is most likely to break functionality if misconfigured - always test in report-only mode first.",[579,592,594],{"question":593},"Do security headers work on static sites?",[18,595,596],{},"Yes, security headers work on static sites. Configure them through your hosting platform: vercel.json for Vercel, _headers file for Netlify, or through your CDN's configuration. Headers are sent by the server regardless of content type.",[579,598,600],{"question":599},"What is the difference between X-Frame-Options and CSP frame-ancestors?",[18,601,602],{},"Both prevent clickjacking. X-Frame-Options only supports DENY, SAMEORIGIN, or ALLOW-FROM (deprecated). CSP frame-ancestors supports multiple domains and wildcards. Use both for browser compatibility, but prefer frame-ancestors for new implementations.",[579,604,606],{"question":605},"Do I need all these headers if I use a framework like React or Next.js?",[18,607,608],{},"Yes. Frameworks help with some security aspects but don't add HTTP headers automatically. You must configure security headers separately through your hosting platform or server configuration.",[610,611,614,618],"cta-box",{"href":612,"label":613},"/","Start Free Scan",[50,615,617],{"id":616},"check-your-security-headers","Check Your Security Headers",[18,619,620],{},"Run a free scan to see which security headers your site is missing and get specific recommendations.",[622,623,624,630,635,640],"related-articles",{},[625,626],"related-card",{"description":627,"href":628,"title":629},"Configure headers via vercel.json and middleware.","/blog/how-to/vercel-headers","Security Headers on Vercel",[625,631],{"description":632,"href":633,"title":634},"Use _headers file and netlify.toml for headers.","/blog/how-to/netlify-headers","Security Headers on Netlify",[625,636],{"description":637,"href":638,"title":639},"Deep dive into CSP directives and configuration.","/blog/how-to/csp-setup","Content Security Policy Setup",[625,641],{"description":642,"href":643,"title":644},"Set up HTTP Strict Transport Security correctly.","/blog/how-to/hsts-setup","HSTS Configuration Guide",{"title":225,"searchDepth":646,"depth":646,"links":647},2,[648,649,650,651,660,665,671,672],{"id":52,"depth":646,"text":53},{"id":72,"depth":646,"text":73},{"id":112,"depth":646,"text":113},{"id":202,"depth":646,"text":203,"children":652},[653,655,656,657,658,659],{"id":211,"depth":654,"text":212},3,{"id":237,"depth":654,"text":238},{"id":257,"depth":654,"text":258},{"id":276,"depth":654,"text":277},{"id":292,"depth":654,"text":293},{"id":312,"depth":654,"text":313},{"id":366,"depth":646,"text":367,"children":661},[662,663,664],{"id":370,"depth":654,"text":371},{"id":404,"depth":654,"text":405},{"id":430,"depth":654,"text":431},{"id":440,"depth":646,"text":441,"children":666},[667,668,669,670],{"id":444,"depth":654,"text":445},{"id":468,"depth":654,"text":469},{"id":507,"depth":654,"text":508},{"id":529,"depth":654,"text":530},{"id":573,"depth":646,"text":574},{"id":616,"depth":646,"text":617},"how-to","2026-01-07","Step-by-step guide to adding security headers. Protect against XSS, clickjacking, and MIME sniffing with CSP, X-Frame-Options, HSTS, and more. Includes code examples for Express, Next.js, and nginx.",false,"md",[679,681,683,685,688],{"question":581,"answer":680},"Every website should have: X-Content-Type-Options: nosniff (prevents MIME sniffing), X-Frame-Options: DENY or SAMEORIGIN (prevents clickjacking), Strict-Transport-Security with max-age of at least 31536000 (enforces HTTPS), Referrer-Policy: strict-origin-when-cross-origin (limits referrer leakage), and Content-Security-Policy (controls resource loading). These headers protect against the most common web attacks.",{"question":587,"answer":682},"Basic headers like X-Content-Type-Options, X-Frame-Options, and Referrer-Policy rarely cause issues. HSTS can cause problems if your site isn't fully HTTPS-ready. Content-Security-Policy is the most likely to break functionality if misconfigured - always test in report-only mode first.",{"question":593,"answer":684},"Yes, security headers work on static sites. Configure them through your hosting platform: vercel.json for Vercel, _headers file for Netlify, or through your CDN's configuration panel. The headers are sent by the server regardless of whether your content is static or dynamic.",{"question":686,"answer":687},"How do I test if my security headers are working?","Open Chrome DevTools (F12), go to the Network tab, reload your page, click on the main document request, and check the Response Headers section. You can also use online tools like securityheaders.com to get a detailed report and grade for your site's security headers.",{"question":599,"answer":689},"Both prevent clickjacking, but CSP frame-ancestors is more flexible and modern. X-Frame-Options only supports DENY, SAMEORIGIN, or ALLOW-FROM (deprecated). CSP frame-ancestors supports multiple domains and wildcards. Use both for maximum browser compatibility, but prefer frame-ancestors for new implementations.","yellow",null,{},true,"Protect your app with HTTP security headers. Complete guide with code examples for all major frameworks.","/blog/how-to/add-security-headers","[object Object]","HowTo",{"title":5,"description":675},{"loc":695},"blog/how-to/add-security-headers",[],"summary_large_image","-cBB8tu1y8Q-WoNyknktA3QRweUE7v3NSkLPkdCxXG4",1775843918546]