CVE-2025-51464
aimhubio Aim: XSS via malicious Python in reports executed by Pyodide.
- CVSS 8.8
- CWE-79
- Input Validation and Sanitization
- Remote
Cross-site Scripting (XSS) in aimhubio Aim 3.28.0 allows remote attackers to execute arbitrary JavaScript in victims browsers via malicious Python code submitted to the /api/reports endpoint, which is interpreted and executed by Pyodide when the report is viewed. No sanitisation or sandbox restrictions prevent JavaScript execution via pyodide.code.run_js().
- CWE
- CWE-79
- CVSS base score
- 8.8
- Published
- 2025-07-22
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Cross-Site Scripting (XSS)
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Aim
- Fixed by upgrading
- Yes
Solution
Upgrade to Aim version 3.28.1 or later.
Vulnerable code sample
import json
from flask import Flask, request, jsonify, render_template_string
# This is a simplified representation of the vulnerable application backend.
# In a real-world scenario, this would be part of a larger application framework.
app = Flask(__name__)
# A simple in-memory "database" to store reports.
REPORTS_DB = {}
# The vulnerable HTML template for rendering a report.
# It loads Pyodide and directly executes the Python code associated with the report.
# This is where the core of the vulnerability lies: the Python code is executed
# in the client's browser without any sanitization.
VULNERABLE_VIEW_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>Aim Report Viewer</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js"></script>
</head>
<body>
<h1>Viewing Report</h1>
<div id="status">Loading Pyodide...</div>
<hr>
<script>
async function main() {
try {
// Load the Pyodide runtime
let pyodide = await loadPyodide();
document.getElementById("status").innerText = "Pyodide loaded. Executing report code...";
// **VULNERABLE PART**
// The python_code from the report is directly passed to runPython.
// It is not sanitized or sandboxed in any way.
const python_code = {{ python_code_json }};
await pyodide.runPythonAsync(python_code);
document.getElementById("status").innerText = "Report execution finished.";
} catch (err) {
document.getElementById("status").innerText = "An error occurred: " + err;
}
}
main();
</script>
</body>
</html>
"""
# API endpoint for submitting a new report.
# An attacker would use this endpoint to submit a report containing malicious Python code.
@app.route('/api/reports', methods=['POST'])
def create_report():
data = request.get_json()
if not data or 'id' not in data or 'python_code' not in data:
return jsonify({'error': 'Invalid report payload'}), 400
report_id = str(data['id'])
# The application stores the user-submitted Python code without any validation or sanitization.
REPORTS_DB[report_id] = data['python_code']
return jsonify({'status': 'Report created successfully', 'report_id': report_id}), 201
# Web page for viewing a report.
# When a victim visits this page, their browser will execute the malicious Python code.
@app.route('/reports/<report_id>')
def view_report(report_id):
# Retrieve the potentially malicious code from the database.
report_code = REPORTS_DB.get(str(report_id), "# Report not found.")
# The code is passed to the template. json.dumps is used to ensure it's a valid
# JavaScript string, but this does not sanitize the content of the string itself.
return render_template_string(
VULNERABLE_VIEW_TEMPLATE,
python_code_json=json.dumps(report_code)
)
if __name__ == '__main__':
# To demonstrate the vulnerability:
# 1. Run this Python script.
# 2. Attacker submits a malicious report via curl:
# curl -X POST http://127.0.0.1:5000/api/reports \
# -H "Content-Type: application/json" \
# -d '{"id": "xss_poc", "python_code": "import pyodide.code\npyodide.code.run_js(\"alert(\'XSS Vulnerability Executed!\')\")"}'
#
# 3. Victim browses to the report page: http://127.0.0.1:5000/reports/xss_poc
# 4. The JavaScript alert will execute in the victim's browser.
app.run(port=5000, debug=False)Patched code sample
import ast
from flask import Flask, request, jsonify
# This is a hypothetical representation of the Aim backend service.
# The code demonstrates a fix for a vulnerability where user-submitted Python code
# could execute arbitrary JavaScript via Pyodide on the frontend.
#
# The vulnerability (CVE-2025-51464) description states that no sanitisation
# prevented the use of `pyodide.code.run_js()`.
#
# THE FIX:
# The fix is to implement server-side validation of the Python code before it is
# stored and sent to the frontend for execution. This validation uses Python's
# Abstract Syntax Tree (AST) module to parse the code and inspect it for
# disallowed patterns, such as importing the 'pyodide' module or calling
# the dangerous 'run_js' function. If a forbidden pattern is detected, the
# request is rejected.
app = Flask(__name__)
# In a real application, this would be a database.
REPORTS_STORAGE = {}
class PyodideCodeValidator(ast.NodeVisitor):
"""
AST visitor to check for disallowed patterns in user-submitted Python code.
It specifically looks for imports of 'pyodide' and calls to 'run_js'.
"""
def __init__(self):
self.is_safe = True
self.errors = []
def visit_Import(self, node):
for alias in node.names:
if alias.name == 'pyodide':
self.is_safe = False
self.errors.append("Direct import of 'pyodide' is not allowed.")
self.generic_visit(node)
def visit_ImportFrom(self, node):
if node.module == 'pyodide':
self.is_safe = False
self.errors.append("Import from 'pyodide' is not allowed.")
self.generic_visit(node)
def visit_Call(self, node):
# Check for calls like `some_object.run_js(...)`
if isinstance(node.func, ast.Attribute) and node.func.attr == 'run_js':
self.is_safe = False
self.errors.append("Calls to 'run_js' are not allowed.")
# Check for simple calls like `run_js(...)` if it were imported directly
elif isinstance(node.func, ast.Name) and node.func.id == 'run_js':
self.is_safe = False
self.errors.append("Calls to 'run_js' are not allowed.")
self.generic_visit(node)
def is_code_safe_for_pyodide(code_string: str) -> (bool, list):
"""
Parses a Python code string into an AST and uses the validator to check it.
Returns a tuple containing a boolean (True if safe) and a list of errors.
"""
try:
tree = ast.parse(code_string)
validator = PyodideCodeValidator()
validator.visit(tree)
return validator.is_safe, validator.errors
except SyntaxError as e:
return False, [f"Invalid Python syntax: {e}"]
@app.route('/api/reports', methods=['POST'])
def create_report():
"""
Endpoint to receive Python code for a new report.
This endpoint now includes the security validation fix.
"""
payload = request.get_json()
if not payload or 'python_code' not in payload:
return jsonify({"error": "Request must include 'python_code' field."}), 400
user_code = payload['python_code']
# --- FIX IMPLEMENTED HERE ---
# The user-submitted code is validated before being accepted.
is_safe, errors = is_code_safe_for_pyodide(user_code)
if not is_safe:
# If the code is not safe, reject the request with an error message.
return jsonify({
"error": "Malicious code pattern detected.",
"details": errors
}), 400
# --- END OF FIX ---
# If the code passes validation, it is stored.
# The frontend can now safely fetch and execute this code with Pyodide,
# confident that it cannot call `run_js` to perform an XSS attack.
report_id = len(REPORTS_STORAGE) + 1
REPORTS_STORAGE[report_id] = {"content": user_code}
return jsonify({
"message": "Report created successfully with validated code.",
"report_id": report_id
}), 201
if __name__ == '__main__':
# To test this fix:
# 1. Run this script.
# 2. Send POST requests to http://127.0.0.1:5000/api/reports
#
# Example of a SAFE request (will be accepted):
# curl -X POST -H "Content-Type: application/json" -d '{"python_code": "x = 1\ny = 2\nprint(f''Result: {x+y}'')"}' http://127.0.0.1:5000/api/reports
#
# Example of a MALICIOUS request (will be rejected by the fix):
# curl -X POST -H "Content-Type: application/json" -d '{"python_code": "import pyodide\npyodide.code.run_js(\"alert(''XSS'');\")"}' http://127.0.0.1:5000/api/reports
#
# Another MALICIOUS example (will be rejected by the fix):
# curl -X POST -H "Content-Type: application/json" -d '{"python_code": "api.run_js(\"alert(''XSS'');\")"}' http://127.0.0.1:5000/api/reports
app.run(port=5000)Payload
from pyodide.code import run_js
run_js("alert('XSS on ' + document.domain)")
Cite this entry
@misc{vaitp:cve202551464,
title = {{aimhubio Aim: XSS via malicious Python in reports executed by Pyodide.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-51464},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-51464/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
