Веб
Неправильная настройка CORS: отражение Origin, wildcard и null
CORS — не защита, а разрешение для браузера. При ошибке чужой сайт читает данные из вашего API от имени пользователя. Проверка через curl и правильная настройка.
· 9 мин. чтения

О CORS (Cross-Origin Resource Sharing) разработчики чаще всего вспоминают в одной ситуации: фронтенд обращается к API, и в консоли браузера появляется красная ошибка. В качестве быстрого решения ставится найденная в интернете настройка «разрешить всё», и ошибка исчезает. Эта настройка уходит в production, и к ней больше никто не возвращается.
В этой статье покажем, что на самом деле делает CORS, какие настройки опасны, как проверить их через curl и как правильно настроить в Laravel, Node и nginx.
Что такое CORS и чем он не является
В браузере действует базовое правило — Same-Origin Policy: JavaScript на странице https://evil.example может отправить запрос на https://api.your-domain.uz, но не может прочитать ответ. Origin — это сочетание схемы, домена и порта: https://app.your-domain.uz и https://api.your-domain.uz — разные origin.
CORS — механизм осознанного исключения из этого правила. Добавляя в ответ заголовки, сервер говорит браузеру: «разреши этому origin прочитать ответ»:
Access-Control-Allow-Origin: https://app.your-domain.uz
Access-Control-Allow-Credentials: true
Два важных вывода:
- CORS — не слой защиты, а слой разрешений. Он ослабляет ограничение, а не усиливает его. Каждое неверное значение — ещё одна открытая дверь.
- CORS работает только в браузере.
curl, сервер или мобильное приложение CORS игнорируют. Поэтому CORS не заменяет авторизацию: API должен сам проверять пользователя в каждом запросе.
Почему это важно для экспертизы
Опасность ошибки CORS в том, что браузер пользователя становится инструментом атаки. Пользователь, будучи авторизованным в вашей системе, открывает чужую страницу. Если API работает с cookie, а CORS доверяет любому origin, скрипт на этой странице отправляет запрос от имени пользователя, читает ответ — профиль, отчёты, токены — и пересылает на свой сервер.
На экспертизе это оценивается как «Security Misconfiguration» или раскрытие информации. Найти такую ошибку просто: хватит нескольких запросов curl. Поэтому этот пункт входит в число настроек, которые обязательно нужно проверить до экспертизы.
Опасный случай 1: отражение Origin + credentials
Самая серьёзная ошибка. Сервер берёт заголовок Origin из запроса, без изменений возвращает его в ответе и разрешает cookie:
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true
Фактически это означает «любой сайт может читать API от имени пользователя». Часто это результат такого кода:
// ОШИБКА
app.use(cors({ origin: true, credentials: true }));
origin: true в пакете cors означает «отражать пришедший Origin».
Опасный случай 2: wildcard
Access-Control-Allow-Origin: * — «читать ответ может любой origin». Браузеры не принимают * вместе с Allow-Credentials: true, поэтому данные, защищённые cookie, напрямую не прочитать. Но * всё равно может быть опасен:
- если API без аутентификации отдаёт ресурсы из внутренней сети или ограниченные по IP;
- если доступ даётся не через cookie, а другим «фоновым» способом (например, разрешением по IP);
- если разработчик позже, раз
*«не работает», заменит его на отражение Origin — а это ведёт к случаю 1.
Для публичных, действительно открытых данных (например, курсов валют) * допустим. Для API личного кабинета — нет.
Опасный случай 3: origin null
В некоторых настройках в списке разрешённых стоит null — обычно его добавляют для тестирования локальных файлов. Но Origin: null может отправить и атакующий: с этим значением уходят запросы из sandbox-iframe, data: URL или страниц file://. Никогда не указывайте null как разрешённый origin.
Опасный случай 4: неверная проверка (regex и endsWith)
Если в коде проверки разрешённых origin есть ошибка, список обходится, даже если он существует:
// ОШИБКА: проходят и evilyour-domain.uz, и your-domain.uz.evil.example
if (origin.endsWith('your-domain.uz') || origin.includes('your-domain.uz')) { ... }
// ОШИБКА: точка не экранирована, нет якорей
/https:\/\/.*.your-domain.uz/
Правильный способ — точное сравнение по списку или строго заякоренный regex:
/^https:\/\/([a-z0-9-]+\.)?your-domain\.uz$/
И ещё: не добавляйте в production-список origin с http:// (без шифрования) — атакующий в сети может внедрять контент от их имени.
Как проверить через curl
Отправьте запрос с чужим origin и посмотрите заголовки Access-Control-* в ответе:
curl -si https://api.your-domain.uz/v1/me \
-H "Origin: https://evil.example" | grep -i access-control
curl -si https://api.your-domain.uz/v1/me \
-H "Origin: null" | grep -i access-control
curl -si https://api.your-domain.uz/v1/me \
-H "Origin: https://your-domain.uz.evil.example" | grep -i access-control
Проверьте и preflight-запрос (OPTIONS) — именно его браузер отправляет перед PUT, DELETE или запросами с особыми заголовками:
curl -si -X OPTIONS https://api.your-domain.uz/v1/profile \
-H "Origin: https://evil.example" \
-H "Access-Control-Request-Method: DELETE" \
-H "Access-Control-Request-Headers: authorization" | grep -i access-control
Как читать результат:
- Чужой origin вернулся в ответе и есть
Allow-Credentials: true— серьёзная находка. - Вернулся
Allow-Origin: null— находка. - Вернулся
*— оценивайте по тому, какие данные отдаёт эндпоинт. - Для чужого origin
Access-Control-Allow-Originнет вовсе — правильно.
Проверьте все основные эндпоинты, включая вход, профиль, скачивание файлов и админский API. Иногда CORS прописан не в общем middleware, а вручную в отдельном контроллере.
Правильная настройка в Laravel
Laravel управляет CORS через встроенный middleware HandleCors и config/cors.php (в Laravel 11+ файл можно опубликовать командой php artisan config:publish cors):
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
'allowed_origins' => [
'https://app.your-domain.uz',
'https://admin.your-domain.uz',
],
'allowed_origins_patterns' => [],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With', 'X-XSRF-TOKEN'],
'exposed_headers' => [],
'max_age' => 600,
'supports_credentials' => true,
];
Обратите внимание:
'*'вallowed_originsиsupports_credentials => trueне должны стоять вместе.- Удобно читать origin из
.envв зависимости от окружения, но production-значение должно быть точным списком доменов. - Если используете
allowed_origins_patterns, regex должен быть заякорен^и$, а точки — экранированы.
Правильная настройка в Node (Express)
const allowed = new Set([
'https://app.your-domain.uz',
'https://admin.your-domain.uz',
]);
app.use(cors({
origin(origin, cb) {
// Нет Origin — запрос не из браузера (curl, сервер); CORS-заголовок не нужен
if (!origin) return cb(null, false);
cb(null, allowed.has(origin));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
maxAge: 600,
}));
При динамическом origin пакет cors добавляет заголовок Vary: Origin. Это важно: иначе прокси или CDN может закэшировать ответ для одного origin и отдать его другому.
На уровне nginx
Если CORS-заголовки добавляет nginx, не возвращайте $http_origin напрямую — сначала проверьте его через map:
map $http_origin $cors_origin {
default "";
"https://app.your-domain.uz" $http_origin;
"https://admin.your-domain.uz" $http_origin;
}
server {
location /api/ {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary Origin always;
# ...
}
}
nginx не отправляет add_header с пустым значением, поэтому origin не из списка заголовок не получит. Настраивайте в одном месте: если CORS добавляют и приложение, и nginx, появляются дублирующиеся заголовки, и браузер отклоняет ответ.
Связанные темы
В API, работающих с cookie, CORS тесно связан с CSRF: CORS управляет чтением ответа, но не останавливает отправку запроса. Поэтому запросы, меняющие состояние, нужно отдельно защищать CSRF-токеном или cookie с SameSite. О том, где хранить токен, мы писали в статье про JWT, а общий список по API — в статье про OWASP API Top 10.
Список CORS — это решение «кому мы доверяем». Он должен быть коротким, точным, и каждая строка в нём — обоснованной.
Чек-лист
- Разрешённые origin — точный список; Origin не отражается.
*иAllow-Credentials: trueне используются вместе; в личном API нет*.- В списке нет
nullи origin сhttp://. - Regex-проверки заякорены, точки экранированы;
endsWith/includesне используются. - При динамическом origin отправляется
Vary: Origin. - Разрешённые методы и заголовки минимальны.
- CORS настроен в одном месте (приложение или nginx).
- API проверяет авторизацию в каждом запросе независимо от CORS.
- Проведена проверка
curlс чужим,nullи похожим доменом.
Итог
Ошибка CORS живёт не в коде, а в одной строке конфигурации, и часто её поставили «временно». Найти и исправить её — дело нескольких минут, просто сделать это нужно до экспертизы.
Хотите проверить, каким origin доверяет ваш API? Оставьте заявку на сканирование — Claude AI агент просмотрит настройки и код, а наш специалист подтвердит находки и поможет их закрыть.


