NGINX is commonly used as a static file server, reverse proxy, TLS terminator, and load balancer.
Its security model is mostly built from server blocks, location blocks, filesystem mappings, and proxy rules.
Where NGINX Appears
Common deployment patterns:
NGINX -> static files
NGINX -> PHP-FPM
NGINX -> gunicorn / uWSGI
NGINX -> Node / Express
NGINX -> Java backend
NGINX -> internal admin service
When NGINX is a reverse proxy, the application may not see the real client directly. It sees the request after NGINX rewrites, normalizes, limits, and forwards it.
Important Paths
Debian and Ubuntu:
/etc/nginx/nginx.conf
/etc/nginx/sites-available/
/etc/nginx/sites-enabled/
/etc/nginx/conf.d/
/var/log/nginx/access.log
/var/log/nginx/error.log
/var/www/html/
/usr/share/nginx/html/
RHEL and CentOS style layouts:
/etc/nginx/nginx.conf
/etc/nginx/conf.d/
/usr/share/nginx/html/
/var/log/nginx/access.log
/var/log/nginx/error.log
Useful local commands when we have shell access:
nginx -t
nginx -T
systemctl status nginx
ps aux | grep nginx
nginx -T prints the full loaded configuration. It is one of the fastest ways to understand the active server layout.
Core Configuration
Basic structure:
events {}
http {
server {
listen 80;
server_name example.local;
root /var/www/html;
index index.html index.php;
location / {
try_files $uri $uri/ =404;
}
}
}
Important directives:
server_name Hostname matched by the server block
root Filesystem root for this context
alias Filesystem mapping for a specific location
location URL matching rule
try_files File resolution order
proxy_pass Backend proxy destination
fastcgi_pass PHP-FPM or FastCGI backend
autoindex Directory listing
client_max_body_size Upload/request body size limit
Location Matching
NGINX location matching is powerful and easy to misunderstand.
Common forms:
location = /admin { }
location /api/ { }
location ^~ /static/ { }
location ~ \.php$ { }
location ~* \.(jpg|png|gif)$ { }
Meaning:
= exact match
^~ preferred prefix match
~ case-sensitive regex
~* case-insensitive regex
/path prefix match
Security issues often appear when access control is placed in one location block but a different location block actually handles the request.
Example risk:
location /admin/ {
allow 10.0.0.0/8;
deny all;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
}
If PHP routing and admin access control are not aligned, a PHP file under /admin/ may be handled by the regex location instead of the intended protected block, depending on the exact configuration.
Root vs Alias
root appends the URI to the configured filesystem path.
location /images/ {
root /var/www;
}
Request:
/images/logo.png
Filesystem:
/var/www/images/logo.png
alias replaces the matched location with the configured filesystem path.
location /images/ {
alias /srv/assets/;
}
Request:
/images/logo.png
Filesystem:
/srv/assets/logo.png
Alias mistakes can expose files outside the intended directory. Pay close attention to trailing slashes on both the location and the alias.
Risk pattern:
location /static {
alias /var/www/app/static/;
}
Safer pattern:
location /static/ {
alias /var/www/app/static/;
}
Reverse Proxy Rules
Typical proxy rule:
location /api/ {
proxy_pass http://127.0.0.1:5000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Review:
- Which paths are proxied?
- Is the upstream local or remote?
- Does the upstream expose admin routes?
- Are forwarded headers trusted safely?
- Does the backend rely on the original scheme or host?
- Are WebSocket or upgrade headers passed?
- Are path prefixes stripped or preserved?
Trailing slashes matter.
proxy_pass http://127.0.0.1:5000;
proxy_pass http://127.0.0.1:5000/;
These can produce different backend paths. When a proxy bug is suspected, compare what the frontend receives with what the backend receives.
PHP-FPM And FastCGI
NGINX does not execute PHP by itself. It usually passes PHP requests to PHP-FPM.
Typical block:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
Review:
- Which extensions reach PHP-FPM?
- Can uploaded files receive a PHP extension?
- Are path info settings safe?
- Is the PHP block restricted to the intended document root?
- Does
try_filesconfirm the script exists before execution?
Safer PHP pattern:
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
The key idea is simple: only real PHP files in the intended webroot should reach PHP-FPM.
Enumeration
Remote checks:
curl -i http://target/
curl -i http://target/nonexistent
curl -I http://target/
whatweb http://target/
nikto -h http://target/
Interesting paths:
/nginx_status
/status
/stub_status
/admin/
/api/
/uploads/
/static/
/.git/
Virtual host checks:
ffuf -u http://TARGET_IP/ -H "Host: FUZZ.example.local" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
If LFI exists, read:
/etc/nginx/nginx.conf
/etc/nginx/sites-enabled/default
/etc/nginx/conf.d/default.conf
/var/log/nginx/access.log
/var/log/nginx/error.log
Common Weaknesses
Exposed Status
NGINX can expose status information through stub_status.
Example config:
location /nginx_status {
stub_status;
}
This should be restricted to trusted hosts.
Risk:
location /nginx_status {
stub_status;
}
Better:
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
Directory Listing
Risk:
location /files/ {
root /var/www;
autoindex on;
}
autoindex on exposes directory contents when no index file exists.
Alias Traversal
Alias and trailing slash mistakes can expose files outside the intended mapping. Review every alias block carefully.
Test suspicious mappings with traversal-style paths only in authorized labs.
Unsafe Upload Handling
Risk pattern:
location /uploads/ {
root /var/www/app;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
}
If uploaded files land under /uploads/ and .php files execute globally, an upload vulnerability may become code execution.
Better pattern:
location /uploads/ {
root /var/www/app;
default_type application/octet-stream;
}
location ~ ^/uploads/.*\.php$ {
return 403;
}
Log Poisoning
NGINX access logs commonly include the request line, status, referrer, and user agent.
Poison marker:
curl -A "ootw-log-marker" http://target/
High-value log files:
/var/log/nginx/access.log
/var/log/nginx/error.log
This only becomes code execution when another bug includes the log through an executable runtime such as PHP.
Proxy Header Trust
If NGINX forwards X-Forwarded-For, the backend must trust it only when it comes from NGINX.
Risk:
client supplies X-Forwarded-For: 127.0.0.1
backend trusts it as the real client IP
Better:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Then configure the backend to trust only the proxy IP, not arbitrary client headers.
Defensive Checklist
- Use
nginx -Tduring reviews to inspect the loaded config. - Keep
server_nameexplicit. - Avoid a catch-all default site exposing internal content.
- Restrict
stub_status. - Keep uploads out of executable paths.
- Block script execution under upload directories.
- Use
try_filesbefore passing PHP to FastCGI. - Review all
aliasdirectives and trailing slashes. - Review every
proxy_passpath rewrite. - Trust forwarded headers only from known proxy IPs.
- Disable directory listing unless intentionally required.
- Protect logs and configuration files from web access.