vibeblame
Guides

How to add security headers to your web app

CSP, HSTS, X-Frame-Options and other headers protect against XSS, clickjacking, and MIME sniffing. Step-by-step for Next.js, Astro, Vue (Nuxt), Angular, Nginx, Apache, and Vercel.

What each header does

HeaderProtects against
Content-Security-PolicyXSS, code injection
Strict-Transport-SecurityProtocol downgrade attacks
X-Frame-OptionsClickjacking
X-Content-Type-OptionsMIME sniffing / XSS
Referrer-PolicyReferrer info leakage
Permissions-PolicyUnwanted browser API access
X-Powered-ByTech stack exposure (should be removed)

Next.js

// next.config.js
const securityHeaders = [
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
]

const nextConfig = {
  poweredByHeader: false,
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }]
  },
}

module.exports = nextConfig

Astro

For static builds (SSG), headers must be set at the server or CDN level — Astro cannot add HTTP headers to static files. Use the Nginx, Vercel, or Netlify config below.

For SSR (with a Node.js or Cloudflare adapter), add headers in middleware:

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware'

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next()
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set('X-Frame-Options', 'SAMEORIGIN')
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')
  response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
  return response
})

Vue (Nuxt)

Option 1 — via routeRules in nuxt.config.ts (works with SSR and Nitro server):

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/**': {
        headers: {
          'X-Content-Type-Options': 'nosniff',
          'X-Frame-Options': 'SAMEORIGIN',
          'Referrer-Policy': 'strict-origin-when-cross-origin',
          'Strict-Transport-Security': 'max-age=63072000; includeSubDomains',
          'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
        },
      },
    },
  },
})

Option 2 — via server middleware (more flexible):

// server/middleware/security-headers.ts
export default defineEventHandler((event) => {
  setResponseHeaders(event, {
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'SAMEORIGIN',
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    'Strict-Transport-Security': 'max-age=63072000; includeSubDomains',
    'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
  })
})

Option 3 — nuxt-security module (most complete, includes CSP auto-configuration):

npx nuxi module add security
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-security'],
  security: {
    headers: {
      xFrameOptions: 'SAMEORIGIN',
      xContentTypeOptions: 'nosniff',
      referrerPolicy: 'strict-origin-when-cross-origin',
      strictTransportSecurity: 'max-age=63072000; includeSubDomains',
      permissionsPolicy: { camera: [], microphone: [], geolocation: [] },
    },
  },
})

For static builds (nuxt generate), use the Nginx, Vercel, or Netlify configs below.


Angular

Angular itself does not control HTTP response headers — they are always set at the server or CDN level.

Angular Universal (SSR) with Express:

// server.ts
import express from 'express'
const app = express()

app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff')
  res.setHeader('X-Frame-Options', 'SAMEORIGIN')
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
  res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
  next()
})

For static Angular builds (without SSR): use the Nginx, Vercel, or Netlify configs below — headers are applied independently of the Angular application code.


Nginx

server {
  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 Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
  server_tokens off;
}

Apache / PHP / WordPress

Add to .htaccess in the site root:

<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 Strict-Transport-Security "max-age=63072000; includeSubDomains"
  Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
  Header unset X-Powered-By
  Header always unset X-Powered-By
</IfModule>

Make sure mod_headers is enabled:

a2enmod headers && systemctl restart apache2

WordPress plugin option: if you don't have access to .htaccess or httpd.conf, use the Headers & API Manager plugin to add headers from the admin panel.

Remove X-Powered-By in PHP:

// At the top of index.php or in functions.php (WordPress)
header_remove('X-Powered-By');

Vercel

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "SAMEORIGIN" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
      ]
    }
  ]
}

For Next.js on Vercel, prefer next.config.js — it takes precedence and is easier to maintain.


Netlify

# netlify.toml
[[headers]]
  for = "/*"
  [headers.values]
    X-Content-Type-Options = "nosniff"
    X-Frame-Options = "SAMEORIGIN"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Strict-Transport-Security = "max-age=63072000; includeSubDomains"
    Permissions-Policy = "camera=(), microphone=(), geolocation=()"

Tilda

Tilda does not allow setting custom HTTP response headers. Headers are controlled by Tilda's infrastructure.

The only workaround is hosting Tilda export (if available on your plan) behind your own Nginx or Cloudflare, where you can add headers. Without that, this cannot be fixed on Tilda's platform.


CSP — start with report-only mode

Content-Security-Policy is the most powerful header but the most likely to break your app. Start with Report-Only — violations are logged to the console, nothing is blocked:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;

Fix all reported violations, then switch to enforcing:

Content-Security-Policy: default-src 'self'; script-src 'self'; ...

Verify

curl -I https://yoursite.com

Or use securityheaders.com for a full graded report. Then run vibeblame again.

How to add security headers to your web app | vibeblame