Web Server Security Testing: Nginx, Apache and IIS Hardening Assessment
The web server sits in front of every application. Its configuration determines what information gets disclosed about your infrastructure, which HTTP methods are allowed, whether directory listing exposes files to the world, and whether security headers are enforced. Yet web server hardening is routinely skipped in application security reviews — the assumption being that "the DevOps team handled it."
This guide covers the security testing methodology for Nginx, Apache, and IIS: reconnaissance, banner analysis, dangerous configuration patterns, HTTP method testing, and security header validation.
Phase 1: Server Fingerprinting and Reconnaissance
Banner and Version Disclosure
The first thing to check is what the server reveals about itself. Detailed version information helps attackers identify known CVEs immediately.
# Check server banner with curl
curl -sI https://example.com | grep -i server
# Server: nginx/1.24.0
# Server: Apache/2.4.57 (Ubuntu)
# Server: Microsoft-IIS/10.0
# Detailed header inspection
curl -sI https://example.com | grep -i "server\|x-powered-by\|x-aspnet\|x-generator"
# Using nmap for version detection
nmap -sV -p 80,443 example.com
nmap -p 80,443 --script http-headers example.com
Headers that commonly disclose technology stack:
Server: Apache/2.4.57 (Ubuntu)— server software and OSX-Powered-By: PHP/8.1.12— backend language and versionX-AspNet-Version: 4.0.30319— .NET version (IIS)X-Generator: Drupal 10— CMS identificationVia: 1.1 proxy-01.example.internal— internal proxy hostnames
Nikto Scan
# Nikto provides comprehensive web server misconfiguration checks
nikto -h https://example.com
# Key findings Nikto reports:
# - Server version disclosure
# - Directory listing enabled
# - Dangerous files (.htaccess, .git, backup files)
# - Default pages and documentation
# - Missing security headers
# - CGI vulnerabilities
# - SSL/TLS configuration issues
Phase 2: HTTP Method Testing
Web servers should only accept the HTTP methods required by the application. Dangerous methods like PUT, DELETE, TRACE, and CONNECT can enable data modification, cross-site tracing attacks, and proxy abuse.
# Test which HTTP methods are accepted
curl -X OPTIONS https://example.com -sv 2>&1 | grep "< Allow\|Allow:"
# Allow: GET, POST, OPTIONS <- safe
# Allow: GET, POST, PUT, DELETE, TRACE <- potentially dangerous
# Test TRACE method (cross-site tracing attack vector)
curl -X TRACE https://example.com -H "X-Custom: injection-test"
# Vulnerable: server echoes back the request including all headers
# This can expose HttpOnly cookies in some XSS scenarios
# Test PUT method
curl -X PUT https://example.com/test.txt -d "test content"
# 201 Created means arbitrary file upload is possible — critical
# Test DELETE
curl -X DELETE https://example.com/some-file.txt
# Should return 405 Method Not Allowed
# Automated method enumeration with nmap
nmap -p 80,443 --script http-methods --script-args http-methods.url-path='/' example.com
Phase 3: Directory Listing
Directory listing (autoindex) serves an HTML file listing when a directory has no index file. This exposes the entire file structure of directories, including backup files, configuration files, and sensitive documents.
# Test for directory listing on common paths
for path in / /images/ /assets/ /uploads/ /static/ /files/ /backup/ /admin/ /api/; do
status=$(curl -o /dev/null -s -w "%{http_code}" "https://example.com${path}")
body=$(curl -s "https://example.com${path}" | grep -i "index of\|parent directory" | head -1)
[ -n "$body" ] && echo "DIRECTORY LISTING: https://example.com${path}"
done
High-risk directories to check: /uploads/, /backup/, /export/, /temp/, and /logs/ — these are frequently created by developers for file management workflows and forgotten, often with directory listing enabled.
Nginx Autoindex
# Vulnerable nginx configuration
location /files/ {
root /var/www;
autoindex on; # NEVER enable in production
}
# Correct configuration
location /files/ {
root /var/www;
autoindex off; # or simply omit — off is the default
}
Apache DirectoryIndex
# Vulnerable Apache configuration
Options +Indexes # enables directory listing
# Correct configuration
Options -Indexes # disables directory listing
# Or globally in apache2.conf:
#
# Options -Indexes
#
Phase 4: Sensitive File Exposure
Web server misconfigurations can expose files that should never be accessible via HTTP. Check for these common exposures:
# Check for common sensitive file exposures
files=(
"/.git/config"
"/.git/HEAD"
"/.env"
"/.env.local"
"/.env.production"
"/web.config"
"/phpinfo.php"
"/info.php"
"/server-status" # Apache mod_status
"/server-info" # Apache mod_info
"/nginx_status" # Nginx stub_status
"/.htaccess"
"/wp-config.php.bak"
"/config.php.bak"
"/backup.sql"
"/dump.sql"
"/database.sql"
)
for file in "${files[@]}"; do
status=$(curl -o /dev/null -s -w "%{http_code}" "https://example.com${file}")
[ "$status" = "200" ] && echo "EXPOSED: https://example.com${file}"
done
Apache mod_status
# If /server-status returns 200, check what it exposes:
curl https://example.com/server-status?auto
# Exposed information:
# - Current worker processes and states
# - Request URLs being processed (including other users' requests)
# - Client IP addresses
# - CPU usage
# Internal /server-status should never be publicly accessible
Nginx stub_status
# If /nginx_status returns 200:
curl https://example.com/nginx_status
# Exposes: active connections, request counts, reading/writing/waiting states
# Should be restricted to localhost or monitoring IPs only
Phase 5: Security Header Configuration
Security headers are the web server's primary defence layer. Missing or misconfigured headers enable XSS, clickjacking, MIME sniffing, and information disclosure attacks. This is one of the highest-yield areas in a web server assessment.
# Full security header check
curl -sI https://example.com | grep -i \
"strict-transport-security\|content-security-policy\|x-frame-options\|x-content-type-options\|referrer-policy\|permissions-policy\|cross-origin"
| Header | Required Value | Risk if Missing |
|---|---|---|
| Strict-Transport-Security | max-age=31536000; includeSubDomains |
SSL stripping, HTTP downgrade attacks |
| X-Frame-Options | DENY or SAMEORIGIN |
Clickjacking / UI redress attacks |
| X-Content-Type-Options | nosniff |
MIME type confusion, script execution from non-JS resources |
| Content-Security-Policy | Restrictive policy (no unsafe-inline/eval) | XSS execution, data exfiltration |
| Referrer-Policy | strict-origin-when-cross-origin |
URL leakage to third parties |
| Permissions-Policy | Restrict unused browser features | Unwanted browser API access (camera, geolocation) |
Nginx Security Headers Configuration
# Add to nginx.conf or site config:
server {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Remove server version from banner
server_tokens off;
# Remove X-Powered-By from upstream applications
proxy_hide_header X-Powered-By;
fastcgi_hide_header X-Powered-By;
}
Apache Security Headers Configuration
# In .htaccess or apache2.conf (requires mod_headers):
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
# Suppress version disclosure
ServerSignature Off
ServerTokens Prod # shows only "Apache" not the version
# Remove X-Powered-By
Header unset X-Powered-By
Phase 6: TLS Configuration Testing
# Full TLS configuration audit with testssl.sh
./testssl.sh https://example.com
# Or using nmap ssl scripts
nmap -p 443 --script ssl-enum-ciphers example.com
# Using sslyze for structured output
pip install sslyze
python -m sslyze example.com --regular
Key TLS findings to look for:
- TLS 1.0 or 1.1 support (deprecated — disable)
- SSLv2/SSLv3 support (critically deprecated)
- NULL, EXPORT, or RC4 cipher suites
- Missing HSTS header or short max-age
- Certificate expiry within 30 days
- Certificate hostname mismatch
- Self-signed certificate in production
- Missing OCSP stapling
Phase 7: Default and Leftover Content
Default installations of Nginx, Apache, and IIS often include test pages, documentation, and example configurations that should be removed before production deployment.
# Common default pages to check
paths=(
"/index.html" # Default "Welcome to nginx!" page
"/apache2-default/" # Older Apache default
"/icons/" # Apache icons directory
"/manual/" # Apache manual
"/iisstart.htm" # IIS default page
"/welcome.png" # IIS default image
"/aspnet_client/" # ASP.NET client scripts
"/test.php" # Developer test files
"/phpinfo.php" # PHP info page
"/info.php"
"/_profiler/" # Symfony debug toolbar
"/telescope" # Laravel Telescope
"/horizon" # Laravel Horizon
"/_debugbar/" # PHP Debugbar
)
for path in "${paths[@]}"; do
status=$(curl -o /dev/null -s -w "%{http_code}" "https://example.com${path}")
[ "$status" = "200" ] && echo "DEFAULT CONTENT: https://example.com${path} ($status)"
done
Phase 8: Request Smuggling Probe
HTTP request smuggling exploits discrepancies between how a frontend (nginx/Apache as reverse proxy) and backend server parse the Content-Length and Transfer-Encoding headers. Full testing requires Burp Suite Pro, but a quick probe is possible with curl:
# Test if Transfer-Encoding: chunked is processed (CL.TE variant probe)
curl -s -X POST https://example.com/ \
-H "Content-Length: 6" \
-H "Transfer-Encoding: chunked" \
-d "0\r\n\r\nG" \
--http1.1
# Unexpected responses (400 errors, timeouts, or garbled responses)
# may indicate request smuggling vulnerability worth full investigation
Common Findings by Server
| Server | Common Finding | Fix |
|---|---|---|
| Nginx | server_tokens on exposes version |
server_tokens off; |
| Nginx | Missing add_header always — headers not sent on error responses |
Add always to all add_header directives |
| Apache | Options Indexes enables directory listing |
Options -Indexes |
| Apache | mod_status/mod_info publicly accessible | Restrict to 127.0.0.1 only |
| Apache | TRACE method enabled | TraceEnable Off |
| IIS | X-Powered-By: ASP.NET header present |
Remove in web.config: <remove name="X-Powered-By"/> |
| IIS | WebDAV enabled (PUT/DELETE allowed) | Disable WebDAV if not required |
| All | Missing HSTS header | Add Strict-Transport-Security with max-age ≥ 1 year |
Ironimo scans your web server configuration automatically — security headers, TLS settings, information disclosure, and HTTP method misconfigurations — as part of every web application scan.
Start free scanHardening Baseline Checklist
- Suppress server version from banners (
server_tokens off/ServerTokens Prod) - Remove
X-Powered-Byand other technology-disclosure headers - Disable directory listing (
autoindex off/Options -Indexes) - Disable TRACE method (
TraceEnable Offin Apache; default disabled in Nginx) - Restrict HTTP methods to GET, POST, HEAD, OPTIONS only unless PUT/DELETE is required
- Remove or restrict access to server-status, server-info, nginx_status endpoints
- Set all required security headers with the
alwaysflag to ensure they appear on error responses - Disable TLS 1.0 and 1.1; allow only TLS 1.2 and 1.3
- Remove default pages, test files, and documentation directories
- Ensure
.env,.git, and backup file paths return 403 or 404