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:

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:

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 scan

Hardening Baseline Checklist