Справочник по проверкам безопасности Nginx. Для каждой проверки — уязвимый код, правильный код, kill chain и рекомендации.
proxy_pass используется переменная из пользовательского ввода ($arg_*, $http_*), атакующий может заставить nginx отправить запрос на произвольный внутренний сервис. Это позволяет сканировать внутреннюю сеть, обращаться к metadata-сервисам облачных провайдеров (169.254.169.254), красть credentials.GET /api/?target=169.254.169.254/latest/meta-data/location /api/ {
proxy_pass http://$arg_target;
}map $arg_target $backend {
default backend;
"service-a" service-a;
"service-b" service-b;
}
location /api/ {
proxy_pass http://$backend;
}$request_uri, $arg_*, $http_* содержат сырой пользовательский ввод. При подстановке в заголовки или редиректы атакующий может внедрить символы \r\n и добавить произвольные HTTP-заголовки, включая Set-Cookie для угона сессий.GET /redirect%0d%0aSet-Cookie:%20admin=1 HTTP/1.1location /redirect {
return 302 https://example.com$request_uri;
}
add_header X-Custom $http_referer;# $uri — уже нормализован nginx
location /redirect {
return 302 https://example.com$uri;
}
# Не используйте $http_* в заголовках$http_host — это сырой заголовок Host из запроса клиента. Атакующий может подставить произвольный Host, что ведёт к cache poisoning, password reset poisoning (ссылка для сброса пароля указывает на домен атакующего), обходу виртуальных хостов на бэкенде.Host: evil.comhttps://evil.com/reset?token=...proxy_set_header Host $http_host;
# $host — нормализован nginx, # соответствует server_name proxy_set_header Host $host;
location /files (без /) указывает на alias /var/www/uploads (без /), то запрос /files../etc/passwd преобразуется в /var/www/uploads/../etc/passwd = /etc/passwd. Атакующий читает произвольные файлы на сервере.GET /files../etc/shadowlocation /files {
alias /var/www/uploads;
}# Trailing slash и в location, и в alias
location /files/ {
alias /var/www/uploads/;
}root или alias содержит переменную ($host, $uri и др.), атакующий может манипулировать путём для чтения произвольных файлов через path traversal (../).location /dynamic {
root /var/www/$host;
}# Фиксированный путь
location /dynamic {
root /var/www/mysite;
}ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers DES-CBC3-SHA:RC4-SHA:AES128-SHA;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers on;
ssl nginx принимает на порту 443 обычный HTTP. Клиенты ожидают TLS на 443 — получается либо ошибка соединения, либо (хуже) передача данных в открытом виде по каналу, который считается защищённым.listen 443;
listen 443 ssl http2;
server {
listen 443 ssl;
# Нет ssl_dhparam
}# Генерация: openssl dhparam -out /etc/ssl/dhparam.pem 4096 ssl_dhparam /etc/ssl/dhparam.pem;
.git содержит полную историю репозитория: исходный код, пароли в коммитах, приватные ключи, конфигурации БД. Инструменты вроде git-dumper автоматически восстанавливают репозиторий по HTTP.GET /.git/HEAD → подтверждение наличия репозиторияgit-dumper https://target.com/.git/ repo/git log --all -p | grep -i password → утечка секретов из историиserver {
root /var/www/html;
# .git доступен по HTTP
}location ~ /\.git {
deny all;
return 404;
}.env содержит секреты приложения (DB_PASSWORD, API_KEY). .htpasswd — хэши паролей. .ssh/ — приватные SSH-ключи. Всё это доступно одним GET-запросом.server {
root /var/www/html;
# нет блокировки dotfiles
}# Блокируем все dotfiles кроме .well-known (ACME)
location ~ /\.(?!well-known) {
deny all;
}db.sql, backup.tar.gz, config.bak часто содержат полные дампы БД, конфигурации с паролями. Автоматические сканеры (nuclei, dirsearch) проверяют эти расширения в первую очередь.server {
root /var/www/html;
# db_dump.sql доступен
}location ~* \.(bak|sql|tar|gz|zip|swp|old|orig|dump)$ {
deny all;
}deny all; после allow nginx разрешает доступ ВСЕМ. Директивы allow/deny работают по принципу first match — без завершающего deny all белый список бесполезен.location /internal {
allow 10.0.0.0/8;
# нет deny all — доступ для всех!
}location /internal {
allow 10.0.0.0/8;
deny all;
}return обрабатывается на фазе rewrite, которая выполняется раньше фазы access (allow/deny). Поэтому return 200 в том же location что и deny all делает deny бесполезным — ответ отдаётся до проверки IP.location /admin {
allow 10.0.0.0/8;
deny all;
return 200 "admin"; # deny не работает!
}location /admin {
allow 10.0.0.0/8;
deny all;
proxy_pass http://admin_backend;
}
# Или используйте if + return в отдельном блокеlocation /admin {
proxy_pass http://backend;
}location /admin {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
allow 10.0.0.0/8;
deny all;
proxy_pass http://backend;
}set_real_ip_from 0.0.0.0/0 доверяет заголовку X-Forwarded-For от ЛЮБОГО источника. Атакующий подставляет в X-Forwarded-For любой IP, обходя allow/deny правила, rate limiting и geo-блокировки.curl -H "X-Forwarded-For: 10.0.0.1" https://target.com/internalallow 10.0.0.0/8; deny all; → доступ к внутренним ресурсамset_real_ip_from 0.0.0.0/0; real_ip_header X-Forwarded-For;
# Только IP вашего балансировщика/CDN set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; real_ip_header X-Forwarded-For; real_ip_recursive on;
resolver 8.8.8.8;
resolver 127.0.0.1 valid=30s; # Или корпоративный DNS-сервер resolver 10.0.0.53;
none в valid_referers разрешает запросы БЕЗ заголовка Referer. Любой инструмент (curl, скрипт) по умолчанию не отправляет Referer, полностью обходя защиту от хотлинкинга.valid_referers none server_names;
if ($invalid_referer) {
return 403;
}valid_referers server_names
*.example.com;
if ($invalid_referer) {
return 403;
}try_files $uri =404; nginx передаёт в PHP-FPM запросы к несуществующим .php файлам. Атакующий загружает картинку с PHP-кодом (shell.jpg), затем обращается к /uploads/shell.jpg/x.php — PHP-FPM исполняет код из картинки.shell.jpg содержит <?php system($_GET['c']); ?>GET /uploads/shell.jpg/.phplocation ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}location ~ \.php$ {
try_files $uri =404; # ← обязательно!
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}rewrite ^/loop/(.*)$ /loop/$1 last;
# Перезапись не должна совпадать с входом rewrite ^/old/(.*)$ /new/$1 last;
proxy_redirect бэкенд может вернуть Location-заголовок с внутренним IP (например, 302 http://10.0.1.5:8080/login). Это раскрывает топологию внутренней сети — полезная информация для дальнейших атак.location / {
proxy_pass http://backend:3000;
# внутренний IP может утечь
}location / {
proxy_pass http://backend:3000;
proxy_redirect default;
}autoindex on показывает все файлы в директории. Атакующий может найти backup-файлы, конфигурации, логи, временные файлы, которые не предназначены для публичного доступа.autoindex on;
# Удалите autoindex on
# Или ограничьте доступ
location /public-files/ {
autoindex on;
allow 10.0.0.0/8;
deny all;
}server {
# нет security-заголовков
}add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Content-Security-Policy "default-src 'self'" always;
add_header, ВСЕ заголовки из родительского server/http-блока сбрасываются. Security-заголовки (X-Frame-Options, HSTS) молча исчезают из ответа для этого location.server {
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
location /api/ {
add_header X-Api-Version "1.0";
# ↑ X-Frame-Options и X-Content-Type
# теперь НЕ отдаются для /api/!
}
}server {
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
location /api/ {
# Повторяем все нужные заголовки
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-Api-Version "1.0";
}
}server {
# нет limit_req / limit_conn
}limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
limit_conn conn 20;
location /api/ {
limit_req zone=api burst=20 nodelay;
}
}Server: nginx/1.18.0 раскрывает точную версию. Атакующий может подобрать эксплойт для конкретной версии (CVE). Это information disclosure — не уязвимость сама по себе, но упрощает разведку.http {
# server_tokens on по умолчанию
# Ответ: Server: nginx/1.18.0
}http {
server_tokens off;
# Ответ: Server: nginx
}
# Или server_tokens build; для кастомной строки