---
title: "CORS и безопасность заголовков"
description: "Настройка fiber cors для SPA: preflight и Access-Control-Allow-Origin, список AllowOrigins, AllowCredentials, MaxAge и helmet-заголовки безопасности."
course: "Fiber: экспресс-подобный веб-фреймворк на Fasthttp"
url: "https://golangmaster.ru/courses/fiber/lessons/08-cors/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# CORS и безопасность заголовков

*Курс «Fiber: экспресс-подобный веб-фреймворк на Fasthttp», урок 8 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/fiber/lessons/08-cors/*

Тема «fiber cors» — про договор между браузером и вашим API. До сих пор мы проверяли сервис curl-ом, и ни один слой не спрашивал, откуда пришёл запрос: у curl нет origin. Браузер устроен строже: он подчиняется Same-Origin Policy и не даст скрипту страницы с localhost:3000 прочитать ответ API на localhost:8080, если сервер явно не разрешит. Разберём настройки CORS в Go и Fiber: preflight-запрос OPTIONS и заголовки Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, Access-Control-Allow-Headers; сверим каждое поле Config с исходниками тега v3.5.0, соберём связку SPA на :3000 плюс API на :8080 и добавим helmet-подобные заголовки безопасности — в Fiber v3 для них есть встроенный пакет middleware/helmet.

### fiber cors: Same-Origin Policy и запрос с чужого origin

Same-Origin Policy — базовая политика безопасности браузера: скрипт, загруженный с одного origin, не вправе читать ответы, пришедшие с другого. Origin — это тройка «схема + хост + порт»: http://localhost:3000 и http://localhost:8080 — два разных origin, даже если живут на одной машине; https://example.com и http://example.com — тоже разные, ведь схема и порт входят в origin. Политика защищает пользователя: страница банка не должна уметь дёргать API почтового сервиса с вашими cookies и читать результат.

Проблема всплывает, когда фронтенд и API вы разворачиваете отдельно: SPA крутится на localhost:3000, API — на localhost:8080, и без дополнительных заголовков браузер заблокирует чтение ответов — по умолчанию запрещено всё, что не совпадает по схеме, хосту и порту.

CORS — Cross-Origin Resource Sharing — способ сервера честно объявить браузеру: ответы для этого origin читать можно. Формулировка из документации Fiber переворачивает интуицию: CORS — не защитный механизм, он лишь ослабляет same-origin policy, позволяя кросс-доменным запросам состояться. Настоящая защита — молчаливый запрет браузера, который действует до всякого CORS; ваш слой решает, какую часть запрета снять. Отсюда два вывода, к которым мы вернёмся: список разрешённых origin — это список дверей, которые вы открыли, и он должен быть минимальным; а не-браузерные клиенты CORS игнорируют целиком.

### Простые запросы и preflight: как браузер спрашивает разрешение

Кросс-доменные запросы делятся на два класса. Простые (simple) — исторически безопасные: методы GET, HEAD и POST, обычные заголовки вроде Accept и Content-Type из тройки application/x-www-form-urlencoded, multipart/form-data, text/plain. Такой запрос браузер отправляет сразу, а заголовки CORS смотрит уже в ответе: если Access-Control-Allow-Origin разрешает — JS получает данные; если нет — ответ выбрасывается, и в консоли появляется ошибка.

Всё остальное уходит через preflight. Перед запросом с методами PUT, DELETE, PATCH, QUERY, с кастомными заголовками вроде X-API-Key или с Content-Type: application/json браузер сначала отправляет OPTIONS — предварительный запрос без тела — с заголовками Origin (откуда уйдёт реальный запрос), Access-Control-Request-Method (каким методом) и Access-Control-Request-Headers (какие заголовки понесёт). Сервер отвечает статусом без тела и списком разрешений: Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age. Браузер сверяет ответ с планами: совпало — отправляет реальный запрос; нет — реальный запрос не уходит вовсе.

Отсюда следствие, которое потом нас укусит: preflight не несёт ни cookies, ни кастомных заголовков — браузер не может знать заранее, что сервер разрешит. Запрос OPTIONS выглядит для вашего API как полностью анонимный, и любой слой, требующий ключ или сессию, обязан пускать его до CORS-слоя. Порядок регистрации из шестого урока здесь снова решает всё.

### Словарь CORS-заголовков

| Заголовок | Кто шлёт | Что объявляет |
| --- | --- | --- |
| Origin | браузер, каждый CORS-запрос | origin страницы, с которой уходит запрос |
| Access-Control-Request-Method | браузер, только preflight | метод будущего реального запроса |
| Access-Control-Request-Headers | браузер, только preflight | заголовки, которые понесёт реальный запрос |
| Access-Control-Allow-Origin | сервер | какой origin вправе читать ответ |
| Access-Control-Allow-Credentials | сервер | можно ли отправлять cookies и другие учётные данные |
| Access-Control-Allow-Methods / -Headers | сервер, preflight | разрешённые метод и заголовки реального запроса |
| Access-Control-Max-Age | сервер, preflight | сколько секунд кэшировать разрешение |

### Минимальный слой CORS: cors.New() и дефолты

```go
package main

import (
        "log"

        "github.com/gofiber/fiber/v3"
        "github.com/gofiber/fiber/v3/middleware/cors"
)

func main() {
        app := fiber.New()

        // слой CORS на всё приложение: без аргумента — ConfigDefault
        app.Use(cors.New())

        app.Get("/api/notes", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"items": []string{"первая"}})
        })

        log.Fatal(app.Listen(":8080"))
}
```

Знакомый по седьмому уроку рисунок: New(config ...Config) fiber.Handler — фабрика, конфиг опционален. ConfigDefault из config.go: AllowOrigins: []string{"*"}, AllowMethods: GET, POST, HEAD, PUT, DELETE, PATCH, QUERY, AllowCredentials: false, AllowHeaders и ExposeHeaders пусты, MaxAge: 0. Метод QUERY — не опечатка: в constants.go это константа fiber.MethodQuery со значением QUERY и комментарием RFC 10008 — новый HTTP-метод для запросов с телом, который Fiber держит в дефолтном списке CORS. Звёздочка в AllowOrigins означает «читать можно любому origin»: на любой запрос с Origin браузер получит Access-Control-Allow-Origin: *.

### cors настройки go: Config по полям

Все настройки CORS в Go на Fiber живут в одной структуре cors.Config — разберём поля по исходникам тега v3.5.0, от частых к редким.

AllowOrigins []string — список origin, дефолт — []string{"*"}. Тонкость из кода: пустой список при AllowOriginsFunc == nil тоже означает «разрешить всем» — условие allowAllOrigins := len(cfg.AllowOrigins) == 0 && cfg.AllowOriginsFunc == nil; а пустой список с заданной функцией — наоборот, никого не пускает без проверки функцией (заметка в документации). AllowOriginsFunc func(origin string) bool — динамическая проверка, когда origin надо сверять с базой; ей передаются сериализованные origin и литерал "null".

AllowMethods — список методов для ответа на preflight (дефолт выше). AllowHeaders — какие заголовки реального запроса разрешены; по умолчанию пусто, и тогда middleware возвращает эхом то, что попросил браузер. ExposeHeaders — зеркальная вещь для ответов: по умолчанию JS видит лишь базовый набор вроде Cache-Control, Content-Type и Last-Modified, а так вы разрешаете читать свои заголовки — например X-Response-Time из седьмого урока.

MaxAge int — на сколько секунд браузер запомнит preflight-ответ. Ноль — заголовок Access-Control-Max-Age не ставится, документация уточняет: браузер возьмёт свои 5 секунд по умолчанию. Отрицательное значение ставит заголовок в 0 — кэш preflight отключён полностью, каждый кросс-доменный запрос начинается с OPTIONS. DisableValueRedaction bool — про логи и паники: по умолчанию false, и невалидные origin в сообщениях маскируются строкой [redacted]. Next func(c fiber.Ctx) bool — стандартный скипер: вернул true — слой пропущен.

### Проверьте себя: ответ на preflight

**Вопрос:** Приложение с app.Use(cors.New()) без аргументов получило preflight: OPTIONS с Origin: http://localhost:3000 и Access-Control-Request-Method: POST. Что вернёт сервер?

- ✅ 204 No Content и Access-Control-Allow-Origin: *
- 200 OK и Access-Control-Allow-Origin: http://localhost:3000
- 405 Method Not Allowed: OPTIONS не зарегистрирован
- 204 No Content и Access-Control-Allow-Origin: http://localhost:3000

В cors.go preflight-ветка завершается return c.SendStatus(fiber.StatusNoContent) — это 204 без тела. Origin при AllowOrigins со звёздочкой превращается в allowOrigin = "*", поэтому заголовок — звёздочка, а не эхо конкретного origin: точное значение middleware ставит только для разрешённых из списка. 405 не случится: preflight перехвачен слоем до маршрутизации. Вариант с конкретным origin появится, когда AllowOrigins — список, а не звёздочка.

### AllowOrigins: список, субдомены и паника на опечатке

Рабочий конфиг почти всегда — явный список. Механика из cors.go: входящий Origin приводится к нижнему регистру и сравнивается с разрешёнными; при совпадении Access-Control-Allow-Origin получает точное значение Origin из запроса — не звёздочку. Именно поэтому на простые запросы при конфиге-списке слой добавляет Vary: Origin: ответ зависит от заголовка запроса, и кэш не должен отдавать одному origin ответ, сгенерированный для другого.

Формат каждого элемента проверяет normalizeOrigin: url.Parse, запрет пути, query, фрагмента и userinfo; завершающий слэш разрешён и отбрасывается, схема и хост приводятся к нижнему регистру. Невалидный элемент — не ошибка во время запроса, а паника при создании слоя: [CORS] Invalid origin format in configuration: ... — приложение не поднимется с опечаткой вроде http:/localhost:3000.

Поддержаны субдомены: элемент https://*.example.com разрешает любой уровень вложенности — https://sub.example.com и https://sub.sub.example.com — но не сам https://example.com. Отдельная история — литерал null: браузер шлёт Origin: null для file://-страниц, data:-URL и sandboxed iframes; cors.go этот случай распознаёт, но статический список null не примет — normalizeOrigin отвергнет строку без схемы и хоста, такой origin разрешается только через AllowOriginsFunc, если ваша логика того хочет.

### Что уедет в ответе на preflight?

```go
app.Use(cors.New(cors.Config{
        AllowOrigins: []string{"http://localhost:3000"},
}))

// Запрос из браузера:
// OPTIONS /api/notes
// Host: localhost:8080
// Origin: http://localhost:3000
// Access-Control-Request-Method: POST
```

**Что выведет программа?**

- ✅ 204 и Access-Control-Allow-Origin: http://localhost:3000
- 204 и Access-Control-Allow-Origin: *
- 200 и Access-Control-Allow-Origin: http://localhost:3000
- 403 Forbidden: origin не совпал с портом сервера

AllowOrigins — список из одного origin; Origin запроса совпадает, поэтому allowOrigin = originHeaderRaw — точное значение из запроса. Метод OPTIONS с заголовком Access-Control-Request-Method — preflight: слой ставит заголовки и отвечает return c.SendStatus(fiber.StatusNoContent) — 204. Звёздочки не будет: она только у конфига «разрешить всем». 403 тоже не будет: незнакомый Origin слой не бранёт — он просто не поставит заголовок, молчаливый отказ, браузер сам выбросит ответ. Access-Control-Allow-Methods в этом ответе возьмётся из дефолта: GET, POST, HEAD, PUT, DELETE, PATCH, QUERY.

### AllowCredentials: cookies через чужой origin

AllowCredentials — самое строгое поле. Оно отвечает на вопрос: можно ли запросу нести учётные данные — cookies, TLS-сертификат клиента, заголовки авторизации при fetch с credentials: "include". Со стороны Fiber поле делает две вещи. Первая — в setSimpleHeaders при разрешённом конкретном origin ставится Access-Control-Allow-Credentials: true; при звёздочке — не ставится вовсе.

Вторая — защита на старте. В New() стоит проверка: AllowCredentials вместе с wildcard «*» (или с пустым списком без функции — он тоже означает «все») приводит к панике ещё до запуска сервера: [CORS] Configuration error: When 'AllowCredentials' is set to true, 'AllowOrigins' cannot contain a wildcard origin '*'. Please specify allowed origins explicitly or adjust 'AllowCredentials' setting. Причина в спецификации: комбинация «звёздочка + credentials» запрещена браузерами и уязвима — любой сайт смог бы отправлять запросы с cookies пользователя к вашему API. Ошибка конфигурации ловится при регистрации, а не в проде — фабрика из седьмого урока снова оправдывает себя.

Если список origin динамический, берите AllowOriginsFunc: функция получает origin и возвращает bool, middleware поставит Access-Control-Allow-Origin с этим значением. Документация предупреждает жёстко: никогда не возвращайте true для всех origin — особенно при AllowCredentials: это обходной путь мимо запрета звёздочки, и вы потеряете защиту, которую паника пыталась обеспечить. Задали и AllowOrigins, и AllowOriginsFunc — слой предупредит в лог: [CORS] Both 'AllowOrigins' and 'AllowOriginsFunc' have been defined.

### Проверьте себя: конфиг, который не поднимется

**Вопрос:** Что произойдёт при регистрации app.Use(cors.New(cors.Config{AllowOrigins: []string{"*"}, AllowCredentials: true}))?

- ✅ Паника при создании слоя: приложение не поднимется
- Сервер стартует, а браузеры сами отклонят кросс-доменные запросы
- Слой молча заменит звёздочку на эхо Origin из запроса
- Каждый кросс-доменный запрос получит 401

Проверка в New(): cfg.AllowCredentials && allowAllOrigins — panic с сообщением [CORS] Configuration error: When 'AllowCredentials' is set to true, 'AllowOrigins' cannot contain a wildcard origin '*'. Паника случается при создании слоя, до Listen, и это хорошо: дырявая настройка безопасности не доезжает до прода. Fiber не молча чинит конфиг, и никакого 401 от CORS-слоя не бывает — он вообще не отвечает отказами, просто не ставит заголовки.

### Конфиг для связки SPA на :3000 и API на :8080

```go
// API на :8080 разрешает только фронтенд с localhost:3000
app.Use(cors.New(cors.Config{
        // единственный разрешённый origin — адрес SPA
        AllowOrigins: []string{"http://localhost:3000"},

        // реальный запрос понесёт JSON и заголовок стража из седьмого урока
        AllowHeaders: []string{"Content-Type", "X-API-Key"},

        // методов хватит на CRUD заметок
        AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},

        // fetch(..., { credentials: "include" }) отправит cookies
        AllowCredentials: true,

        // preflight-разрешение кэшируется браузером на час
        MaxAge: 3600,
}))
```

Что увидит браузер. На preflight: 204, Access-Control-Allow-Origin: http://localhost:3000, Access-Control-Allow-Credentials: true, Access-Control-Allow-Methods: GET, POST, PUT, DELETE, Access-Control-Allow-Headers: Content-Type, X-API-Key и Access-Control-Max-Age: 3600 — час без повторных OPTIONS. На простом GET с этим Origin: только Access-Control-Allow-Origin и Access-Control-Allow-Credentials — методы и заголовки на простые запросы не ставятся, в cors.go это видно по ветке simple request с комментарием Omit allowMethods and allowHeaders, only used for pre-flight requests. Content-Type: application/json и X-API-Key перечислены не зря: оба делают запрос «непростым» и вызывают preflight.

### Vary, MaxAge и AllowPrivateNetwork: тонкости ответа

Три детали, отличающие понимание от копипасты. Первая — Vary. cors.go добавляет его почти в каждой ветке: на простые запросы при конфиге-списке — Vary: Origin; на preflight — Vary: Access-Control-Request-Method, Access-Control-Request-Headers и Origin (плюс Access-Control-Request-Private-Network, если включён AllowPrivateNetwork); даже запрос без Origin — «вне CORS» — получает Vary: Origin, если не разрешено всем. Это указание кэшам и прокси: ответ зависел от заголовков запроса, нельзя переиспользовать его для другого Origin — иначе один фронтенд получит из кэша чужие разрешения.

Вторая — MaxAge, уже разобранное: ноль — заголовка нет и браузер живёт своими 5 секундами, отрицательное — кэш запрещён заголовком 0, положительное — столько-то секунд.

Третья — AllowPrivateNetwork, по умолчанию false. Это ответ на Chrome Private Network Access: запрос из публичного сайта к серверу в приватной сети — в том числе к localhost — браузер объявляет в preflight заголовком Access-Control-Request-Private-Network: true, и сервер должен согласиться ответом Access-Control-Allow-Private-Network: true. Наша связка «SPA и API на localhost» флаг не требует — публичная граница между origin-ами не пересекается; но если страница из интернета дёргает ваш локальный dev-сервер, вы знаете, за какой переключатель отвечает.

### Пустой AllowHeaders: эхо из запроса

```go
app.Use(cors.New(cors.Config{
        AllowOrigins: []string{"http://localhost:3000"},
        // AllowHeaders не задан — дефолтный пустой список
}))

// Preflight из браузера:
// OPTIONS /api/notes
// Origin: http://localhost:3000
// Access-Control-Request-Method: POST
// Access-Control-Request-Headers: content-type, x-api-key
```

**Что выведет программа?**

- ✅ Access-Control-Allow-Headers: content-type, x-api-key — эхо запроса
- Заголовка Access-Control-Allow-Headers в ответе не будет
- Access-Control-Allow-Headers: * — разрешено всё
- Access-Control-Allow-Headers: GET, POST — вместо заголовков вернутся методы

В cors.go у preflight-ветки есть else: если cfg.AllowHeaders пуст, слой читает Access-Control-Request-Headers из запроса и, если тот непуст, возвращает его же в Access-Control-Allow-Headers. Удобно в разработке, но помните цену: вы разрешаете любые заголовки, которые попросил браузер, — в промышленном конфиге список явный, как в примере выше. Звёздочку middleware не подставляет, а методы живут в своём отдельном заголовке Access-Control-Allow-Methods.

### Порядок слоёв: helmet → cors → страж

Соберём практику: SPA на localhost:3000 — dev-сервер Vite или статика fiber из пятого урока — и API на localhost:8080. Регистрация слоёв в API — это снова луковица из шестого урока, и два решения здесь обязательны.

Первое: cors встаёт РАНЬШЕ стража requireAPIKey из седьмого урока. Preflight не несёт ни cookies, ни X-API-Key — браузер не отправляет учётные данные в предварительный запрос. Если страж зарегистрирован раньше, он первым видит OPTIONS, не находит заголовок ключа и отвечает 401 — preflight провален, реальный запрос браузер даже не начнёт. Слой CORS, напротив, отвечает на preflight сам и пропускает простые запросы дальше со свежими заголовками.

Второе: helmet встаёт раньше cors. Preflight-ветка cors завершается SendStatus(204) без c.Next() — слои, зарегистрированные ПОСЛЕ cors, на preflight-ответ не работают вовсе: луковица дальше не разворачивается. Helmet ставит свои заголовки ДО вызова c.Next(), поэтому, стоя снаружи, он украшает и обычные ответы, и preflight; внутри — и preflight уйдёт без шлема. Правило собирается в цепочку: helmet → cors → логи и страж → маршруты. Обе части проверяются по исходникам: в cors.go — return c.SendStatus(fiber.StatusNoContent) сразу после заголовков; в helmet.go — все c.Set до return c.Next().

### Каркас API: шлем, CORS, страж, маршруты

```go
package main

import (
        "log"

        "github.com/gofiber/fiber/v3"
        "github.com/gofiber/fiber/v3/middleware/cors"
        "github.com/gofiber/fiber/v3/middleware/helmet"
        "github.com/gofiber/fiber/v3/middleware/logger"
)

// страж из седьмого урока: без X-API-Key группа закрыта
func requireAPIKey(key string) fiber.Handler {
        return func(c fiber.Ctx) error {
                if c.Get("X-API-Key") != key {
                        return c.Status(fiber.StatusUnauthorized).JSON(
                                fiber.Map{"error": "неверный API-ключ"},
                        )
                }
                c.Locals("user", "spa-client")
                return c.Next()
        }
}

func main() {
        app := fiber.New()

        // 1: helmet раньше cors — его заголовки попадают и на preflight
        app.Use(helmet.New())

        // 2: cors раньше стража — preflight не несёт X-API-Key
        app.Use(cors.New(cors.Config{
                AllowOrigins:     []string{"http://localhost:3000"},
                AllowHeaders:     []string{"Content-Type", "X-API-Key"},
                AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
                AllowCredentials: true,
                MaxAge:           3600,
        }))

        // 3: лог и защищённая группа из седьмого урока
        app.Use(logger.New())
        api := app.Group("/api", requireAPIKey("s3cret"))
        api.Get("/notes", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"items": []string{"первая", "вторая"}})
        })
        api.Post("/notes", func(c fiber.Ctx) error {
                return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true})
        })

        // 4: health-check вне группы
        app.Get("/healthz", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"ok": true})
        })

        log.Fatal(app.Listen(":8080"))
}
```

Каркас развивает седьмой урок: requireAPIKey и группа /api прежние, добавились два внешних слоя. Лог запроса браузера на POST /api/notes выглядит так: OPTIONS — 204 и только заголовки, страж и хендлер не вызывались; затем POST с X-API-Key — страж пускает, пишет пользователя в locals, хендлер отвечает 201. Запрос с Origin: http://evil.example получит ответ без Access-Control-Allow-Origin — браузер откажется его читать; curl же без Origin вообще получит данные, потому что для слоя это запрос вне CORS. Оба поведения наблюдаемы в терминале — рецепт в конце урока.

### Helmet-подобные заголовки: в Fiber v3 есть встроенный helmet

Helmet — концепт из мира Node.js: middleware, которое надевает «шлем» на каждый ответ, проставляя набор защитных заголовков. Естественный вопрос — есть ли оно в Fiber. Сверим с тегом v3.5.0: да, в комплекте v3 есть встроенный пакет github.com/gofiber/fiber/v3/middleware/helmet — штатный слой каталога middleware с привычными Config и ConfigDefault, а не сторонний аддон. app.Use(helmet.New()) — и ответы уходят с защитными заголовками.

Что стоит по умолчанию: X-Content-Type-Options: nosniff — запрет MIME-sniffing, браузер не станет угадывать тип содержимого поверх вашего Content-Type; X-Frame-Options: SAMEORIGIN — страницу нельзя встроить в iframe чужого сайта, приём кликджекинга (значение DENY — жёстче, запретит даже встраивание с самого себя); Referrer-Policy: no-referrer — адреса страниц не утекают в Referer; три Cross-Origin-заголовка — Opener-Policy: same-origin, Resource-Policy: same-origin, Embedder-Policy: require-corp — изоляция окон и ресурсов между origin-ами; плюс X-DNS-Prefetch-Control: off, X-Download-Options: noopen, X-Permitted-Cross-Domain-Policies: none и Origin-Agent-Cluster: ?1.

Два дефолта — со смыслом. X-XSS-Protection: 0 — ровно ноль, отключение: устаревший браузерный фильтр XSS auditor сам создавал уязвимости, и современная рекомендация — выключить его, а защиту строить на CSP. А Content-Security-Policy по умолчанию пуст: helmet не ставит заголовок, пока вы не зададите строку, потому что универсального CSP не существует — белые списки ресурсов у каждого приложения свои. Туда же Permissions-Policy (пусто) и Strict-Transport-Security: HSTSMaxAge по умолчанию 0, заголовок ставится только на HTTPS-запросах — c.Secure() — и только при положительном MaxAge; строку max-age=...; includeSubDomains слой собирает один раз в теле фабрики — тот же приём «подготовка до return», что мы разбирали в седьмом уроке.

### helmet.New() с настройкой CSP и HSTS

```go
// Шлем на всё приложение: CSP и HSTS включаются явно
app.Use(helmet.New(helmet.Config{
        // скрипты и стили — только с самого приложения
        ContentSecurityPolicy: "default-src 'self'",

        // HSTS: два года только для HTTPS, включая субдомены
        HSTSMaxAge: 63072000,
}))
```

Правило простое: поле пустое — заголовка нет, строка непустая — ставится. ContentSecurityPolicy уедет как Content-Security-Policy, а при CSPReportOnly: true — как Content-Security-Policy-Report-Only: браузер сообщает о нарушениях, но не блокирует — удобный режим обкатки политики. HSTSMaxAge 63072000 — классические два года; HSTSExcludeSubdomains: true уберёт includeSubDomains, HSTSPreloadEnabled добавит preload. Исходники паникуют на несовместимых настройках: helmet: HSTSPreloadEnabled requires HSTSExcludeSubdomains to be false и helmet: HSTSMaxAge must be greater than or equal to 0 при отрицательном значении.

### Дефолты helmet.New()

| Заголовок | Значение по умолчанию | От чего защищает |
| --- | --- | --- |
| X-Content-Type-Options | nosniff | подмена типов: браузер не угадывает MIME поверх Content-Type |
| X-Frame-Options | SAMEORIGIN | кликджекинг: встраивание страницы в чужой iframe |
| Referrer-Policy | no-referrer | утечка адресов страниц в заголовке Referer |
| Cross-Origin-Opener-Policy | same-origin | изоляция окна: чужие origin не получают доступ к window |
| X-XSS-Protection | 0 | отключение устаревшего браузерного фильтра XSS |
| Content-Security-Policy | не ставится (пусто) | белые списки ресурсов — задаётся вручную под приложение |

### Проверьте себя: чего в шлеме нет

**Вопрос:** Какой заголовок НЕ появится в ответе после app.Use(helmet.New()) без аргументов?

- ✅ Content-Security-Policy: он пуст по умолчанию
- X-Content-Type-Options: nosniff
- X-Frame-Options: SAMEORIGIN
- Referrer-Policy: no-referrer

В ConfigDefault поля ContentSecurityPolicy и PermissionPolicy — пустые строки, а helmet.go ставит заголовок только при непустом значении: if cfg.ContentSecurityPolicy != "". Универсальной политики безопасности не существует — её приносит только ваш конфиг. Остальные три варианта — дефолтные значения, они в ответе будут.

### Соберите регистрацию слоёв API

Расставьте строки: сначала шлем, потом CORS, затем лог, страж и маршруты; запуск — последним.

Правильный порядок строк:

1. `app := fiber.New()`
2. `app.Use(helmet.New())`
3. `app.Use(cors.New(cors.Config{AllowOrigins: []string{"http://localhost:3000"}}))`
4. `app.Use(logger.New())`
5. `api := app.Group("/api", requireAPIKey("s3cret"))`
6. `api.Post("/notes", createNote)`
7. `log.Fatal(app.Listen(":8080"))`

### Допишите CORS-конфиг

Вставьте пропуски: поле списка origin, заголовок стража, разрешение cookies и срок кэша preflight.

```go
app.Use(cors.New(cors.Config{
        // единственный разрешённый origin фронтенда
        ___: []string{"http://localhost:3000"},

        // заголовки реального запроса, которые готов принять сервер
        AllowHeaders: []string{"Content-Type", "___"},

        // разрешаем cookies в кросс-доменных запросах
        AllowCredentials: ___,

        // кэшировать preflight-разрешение на час
        ___: 3600,
}))
```

Подсказка: Поле списка — AllowOrigins; страж из седьмого урока читает X-API-Key; cookies — AllowCredentials: true; срок кэша preflight — MaxAge в секундах.

Правильные ответы в пропусках: `AllowOrigins`, `X-API-Key`, `true`, `MaxAge`

> **CORS — не авторизация и не защита API.** Access-Control-Allow-Origin отвечает на вопрос «какому фронтенду браузер разрешит читать ответы», а не «кто допускается к API». curl, серверные клиенты, боты и библиотеки не исполняют Same-Origin Policy: они шлют запросы без Origin и без всяких CORS-заголовков, и слой пропускает их как запросы «вне CORS». Список AllowOrigins не прячет данные от мира — он лишь снимает браузерное ограничение для избранных. Поэтому страж requireAPIKey("s3cret") из седьмого урока остаётся обязательным: CORS и аутентификация решают разные задачи и работают в паре, а не вместо друг друга.

> **[redacted] в паниках и логах.** Слой бережёт ваши логи: невалидные origin в панике Invalid origin format и в предупреждениях маскируются строкой [redacted] — конфигурация может содержать адреса стейджинга и внутренних сервисов, которые не должны светиться в алертах. Для отладки задайте DisableValueRedaction: true и увидите исходные значения. Ещё два предупреждения log.Warn: заданы и AllowOrigins, и AllowOriginsFunc одновременно; а также AllowOrigins is set to allow all origins, AllowOriginsFunc will not be used — функция игнорируется при звёздочке.

### Словарь урока

- **preflight** — Предварительный OPTIONS-запрос браузера перед «непростым» кросс-доменным запросом: Origin плюс Access-Control-Request-Method (и Request-Headers). Не несёт cookies и кастомных заголовков; ответ сервера — 204 с разрешениями.
- **Access-Control-Allow-Origin** — Главный ответный заголовок CORS: какой origin вправе читать ответ. Звёздочка — всем; конкретное значение — эхо Origin из запроса для разрешённых. Заголовка нет — браузер выбрасывает ответ.
- **AllowOrigins** — Поле cors.Config: список origin, дефолт — звёздочка. Поддерживает https://*.example.com — любые уровни субдоменов, но не сам домен. Пустой список без Func — тоже «все». Невалидный элемент — паника при регистрации.
- **AllowCredentials** — Разрешение учётных данных: cookies и fetch с credentials: include. Ставит Access-Control-Allow-Credentials: true. Со звёздочкой — паника на старте: комбинация запрещена спецификацией.
- **MaxAge** — Секунды кэша preflight-разрешения (Access-Control-Max-Age). Ноль — заголовка нет, браузер использует свои 5 секунд; отрицательный — 0, кэш отключён; 3600 — час.
- **helmet** — Штатный middleware Fiber v3 — middleware/helmet: nosniff, X-Frame-Options SAMEORIGIN, Referrer-Policy no-referrer, серия Cross-Origin-заголовков; CSP и HSTS включаются явно. Ставит заголовки до c.Next() — регистрируется раньше cors.

### Запуск, проверка curl-ом и что дальше

Соберите каркас в main.go и прогоните три сценария. Первый — простой запрос с Origin: curl -i -H "Origin: http://localhost:3000" http://localhost:8080/api/notes — в ответе 200 и заголовки Access-Control-Allow-Origin: http://localhost:3000 и Access-Control-Allow-Credentials: true. Тот же curl без -H Origin вернёт данные без единого CORS-заголовка — для слоя это запрос вне CORS; вот почему проверку делают с Origin, иначе вы тестируете не то. Второй — preflight: curl -i -X OPTIONS http://localhost:8080/api/notes -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, X-API-Key" — 204, полный набор разрешений, Access-Control-Max-Age: 3600 и шлем: X-Content-Type-Options: nosniff. Третий — чужой origin: с Origin: http://evil.example заголовка Access-Control-Allow-Origin в ответе не будет вовсе — молчаливый отказ, на котором браузер и спотыкается. А запрос без X-API-Key покажет 401 от стража седьмого урока — CORS позволил браузеру дотянуться, но не открыл дверь.

Дальше по плану — девятый урок про JSON: байты, которые едут под всеми этими заголовками, ответы c.JSON и разбор входящих запросов. Освежить стража и c.Locals — в [седьмом уроке](/courses/fiber/lessons/07-svoi-middleware/), механику луковицы и порядка слоёв — в [шестом](/courses/fiber/lessons/06-middleware/), полный план — на странице [курса Fiber](/courses/fiber/). В браузерной песочнице сайта сетевые приложения не запускаются — runner-ов в уроках Fiber нет, проверяйте примеры в терминале с локальным Go.

## Коротко: что запомнить

- Same-Origin Policy запрещает браузеру читать ответы чужого origin; CORS — серверное разрешение снять запрет для перечисленных origin. CORS — не защита API: не-браузерные клиенты его игнорируют.
- Preflight — предварительный OPTIONS с Origin, Access-Control-Request-Method и Access-Control-Request-Headers; не несёт cookies и ключей. Fiber отвечает 204 No Content и набором разрешений.
- cors.Config: AllowOrigins (дефолт со звёздочкой; пустой список без Func — тоже все), AllowMethods GET, POST, HEAD, PUT, DELETE, PATCH, QUERY, AllowHeaders, ExposeHeaders, MaxAge (0 — без заголовка и 5 секунд браузера; отрицательный — кэш отключён), AllowCredentials, AllowPrivateNetwork.
- AllowCredentials: true со звёздочкой — паника при регистрации; для разрешённого origin ставятся Access-Control-Allow-Credentials: true и точное значение Access-Control-Allow-Origin.
- Порядок слоёв: helmet раньше cors — preflight-ответ (204 без c.Next()) не доходит до слоёв ниже; cors раньше стража — preflight не несёт X-API-Key.
- helmet — штатный middleware/helmet в v3: nosniff, SAMEORIGIN, no-referrer и серия Cross-Origin-заголовков по умолчанию; CSP, Permissions-Policy и HSTS включаются явно, HSTS — только на c.Secure() и при HSTSMaxAge > 0.
- Vary: Origin и Access-Control-Request-* — указание кэшам; curl без Origin уходит мимо CORS-заголовков, тестировать нужно с -H "Origin: ...".

## Домашнее задание

Развейте каркас урока: добавьте в AllowOrigins второй origin, например http://localhost:5173, и проверьте curl-ом, что Access-Control-Allow-Origin повторяет Origin из запроса, а не выдаёт весь список; соберите helmet-конфиг с ContentSecurityPolicy "default-src 'self'" и CSPReportOnly: true и найдите Content-Security-Policy-Report-Only в ответе; включите AllowPrivateNetwork и отработайте preflight с заголовком Access-Control-Request-Private-Network: true; зарегистрируйте страж раньше cors и в комментарии объясните, почему preflight сломается; наконец задайте MaxAge: -1 и убедитесь, что Access-Control-Max-Age в ответе равен 0.

## Вопросы и ответы по уроку

### Почему браузер шлёт OPTIONS, хотя я вызываю только fetch с POST?

Запрос «непростой»: Content-Type: application/json и/или кастомные заголовки вроде X-API-Key выводят его из списка простых. Браузер сначала спрашивает разрешение preflight-ом — OPTIONS с Origin и Access-Control-Request-Method, — дожидается 204 с Access-Control-Allow-*, и только потом отправляет реальный POST. Заголовок Access-Control-Max-Age сокращает число OPTIONS.

### Access-Control-Allow-Origin со звёздочкой — это дыра в безопасности?

Само по себе нет: звёздочка лишь разрешает любому фронтенду читать ответы вашего API из браузера. Опасна комбинация с AllowCredentials: true — cookies пользователя уходят с запросом; Fiber паникует при такой конфигурации ещё на старте. И помните: звёздочка ничего не запрещает curl-клиентам — от них защищает аутентификация, а не CORS.

### Есть ли в Fiber v3 встроенный helmet?

Да, middleware/helmet в комплекте v3 — штатный слой с Config и ConfigDefault. По умолчанию ставит X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: no-referrer и серию Cross-Origin-заголовков; Content-Security-Policy и HSTS включаются явной настройкой. Регистрируйте его раньше cors, чтобы заголовки попадали и на preflight-ответы.
