vibeblame

Как добавить security headers в веб-приложение

CSP, HSTS, X-Frame-Options и другие заголовки защищают от XSS, clickjacking и MIME sniffing. Пошагово для Next.js, Astro, Vue (Nuxt), Angular, Nginx, Apache и Vercel.

Что делает каждый заголовок

ЗаголовокЗащищает от
Content-Security-PolicyXSS, инъекций кода
Strict-Transport-SecurityАтак на понижение протокола
X-Frame-OptionsClickjacking
X-Content-Type-OptionsMIME sniffing / XSS
Referrer-PolicyУтечки данных через заголовок Referer
Permissions-PolicyНежелательного доступа к API браузера
X-Powered-ByРаскрытия стека (нужно убрать)

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

Для статических сборок (SSG) заголовки нужно выставлять на уровне сервера или CDN — Astro не может добавить HTTP-заголовки к статическим файлам. Используйте конфигурацию Nginx, Vercel или Netlify ниже.

Для SSR (с адаптером Node.js или Cloudflare) добавьте заголовки в 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)

Вариант 1 — через routeRules в nuxt.config.ts (работает с SSR и Nitro-сервером):

// 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=()',
        },
      },
    },
  },
})

Вариант 2 — через серверный middleware (более гибкий):

// 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=()',
  })
})

Вариант 3 — модуль nuxt-security (наиболее полный, включает авто-настройку CSP):

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: [] },
    },
  },
})

Для статических сборок (nuxt generate) используйте конфигурации Nginx, Vercel или Netlify ниже.


Angular

Angular не управляет HTTP-заголовками ответа — они всегда задаются на уровне сервера или CDN.

Angular Universal (SSR) с 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()
})

Для статических Angular-сборок (без SSR): используйте конфигурации Nginx, Vercel или Netlify — заголовки применяются независимо от кода Angular-приложения.


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

Добавьте в .htaccess в корне сайта:

<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>

Убедитесь, что включён mod_headers:

a2enmod headers && systemctl restart apache2

Вариант через плагин WordPress: если нет доступа к .htaccess или httpd.conf, используйте плагин Headers & API Manager.

Убрать X-Powered-By через PHP:

// В начале index.php или в 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=()" }
      ]
    }
  ]
}

Для Next.js на Vercel лучше использовать next.config.js — он имеет приоритет и проще поддерживается.


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 не поддерживает установку кастомных HTTP-заголовков — они контролируются инфраструктурой платформы.

Единственный обходной путь — разместить экспортированный сайт (если ваш план это поддерживает) за своим Nginx или Cloudflare. Без этого проблему на Tilda не решить.


CSP — начните с report-only режима

Content-Security-Policy — самый мощный заголовок, но может сломать приложение. Начните с Report-Only: нарушения логируются в консоль, ничего не блокируется:

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

Устраните нарушения из консоли, затем переключитесь на жёсткий режим:

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

Проверка

curl -I https://yoursite.com

Или используйте securityheaders.com для полного отчёта с оценкой. После — запустите vibeblame снова.

Как добавить security headers в веб-приложение | vibeblame