VAITP Dataset

← Back to the dataset

CVE-2026-33140

PySpector's HTML report is vulnerable to stored XSS via scanned code.

  • CVSS 5.3
  • CWE-79
  • Input Validation and Sanitization
  • Local

PySpector is a static analysis security testing (SAST) Framework engineered for modern Python development workflows. PySpector versions 0.1.6 and prior are affected by a stored Cross-Site Scripting (XSS) vulnerability in the HTML report generator. When PySpector scans a Python file containing JavaScript payloads (i.e. inside a string passed to eval() ), the flagged code snippet is interpolated into the HTML report without sanitization. Opening the generated report in a browser causes the embedded JavaScript to execute in the browser's local file context. This issue has been patched in version 0.1.7.

CVSS base score
5.3
Published
2026-03-20
OWASP
A03 Injection
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Cross-Site Scripting (XSS)
Accessibility scope
Local
Impact
Arbitrary Code Execution
Affected component
PySpector
Fixed by upgrading
Yes

Solution

Upgrade to PySpector version 0.1.7 or later.

Vulnerable code sample

import os

# This class simulates the vulnerable behavior of PySpector <= 0.1.6
class VulnerablePySpector:
    """
    A simplified representation of PySpector's vulnerable report generator.
    """

    def __init__(self):
        self.findings = []

    def scan_file(self, filepath):
        """
        Scans a file for dangerous patterns, like 'eval()'.
        """
        print(f"[*] Scanning file: {filepath}")
        try:
            with open(filepath, 'r') as f:
                for line_num, line in enumerate(f, 1):
                    # A simple check for a potentially dangerous function call
                    if "eval(" in line:
                        finding = {
                            "file": filepath,
                            "line_number": line_num,
                            "code": line.strip(),
                            "description": "Use of eval() is discouraged."
                        }
                        self.findings.append(finding)
                        print(f"[!] Found potential vulnerability on line {line_num}: {line.strip()}")
        except FileNotFoundError:
            print(f"[E] File not found: {filepath}")

    def generate_html_report(self, report_filename="vulnerable_report.html"):
        """
        Generates an HTML report from the findings.
        This function contains the Stored XSS vulnerability.
        """
        if not self.findings:
            print("[*] No findings to report.")
            return

        print(f"[*] Generating HTML report: {report_filename}")

        # Basic HTML structure
        html_content = """
        <html>
        <head>
            <title>PySpector Scan Report</title>
            <style>
                body { font-family: sans-serif; }
                table { border-collapse: collapse; width: 100%; }
                th, td { border: 1px solid #dddddd; text-align: left; padding: 8px; }
                tr:nth-child(even) { background-color: #f2f2f2; }
                th { background-color: #4CAF50; color: white; }
                .code-snippet { font-family: monospace; background-color: #eee; }
            </style>
        </head>
        <body>
            <h1>PySpector Scan Report</h1>
            <table>
                <tr>
                    <th>File</th>
                    <th>Line</th>
                    <th>Description</th>
                    <th>Vulnerable Code Snippet</th>
                </tr>
        """

        # VULNERABLE PART:
        # The 'finding["code"]' is directly embedded into the HTML without any sanitization.
        # If the code contains a JavaScript payload, it will be rendered by the browser.
        for finding in self.findings:
            html_content += f"""
                <tr>
                    <td>{finding['file']}</td>
                    <td>{finding['line_number']}</td>
                    <td>{finding['description']}</td>
                    <td class="code-snippet">{finding['code']}</td>
                </tr>
            """

        html_content += """
            </table>
        </body>
        </html>
        """

        with open(report_filename, 'w') as f:
            f.write(html_content)
        print(f"[+] Report successfully generated. Open '{report_filename}' in a browser to see the result.")


if __name__ == '__main__':
    # 1. Define the malicious Python source code to be scanned.
    # It contains a JavaScript payload within a string passed to eval().
    malicious_code = 'user_input = \'<script>alert("XSS Vulnerability Triggered from PySpector Report!");</script>\'\neval(user_input)\n'
    malicious_filename = "vulnerable_app.py"

    # 2. Create the malicious Python file on disk.
    print(f"[*] Creating a dummy Python file with a payload: '{malicious_filename}'")
    with open(malicious_filename, 'w') as f:
        f.write(malicious_code)

    # 3. Initialize and run the vulnerable scanner.
    scanner = VulnerablePySpector()
    scanner.scan_file(malicious_filename)

    # 4. Generate the vulnerable report.
    scanner.generate_html_report()

    # 5. Clean up the dummy file.
    os.remove(malicious_filename)

Patched code sample

import html
import os

# This class is a mock representation of a finding from a PySpector scan.
class ScanFinding:
    """A simple data class to hold scan finding details."""
    def __init__(self, file_path, line_number, code_snippet, description):
        self.file_path = file_path
        self.line_number = line_number
        self.code_snippet = code_snippet
        self.description = description

class ReportGenerator:
    """
    Represents the HTML report generator for PySpector.
    This demonstrates the fix for CVE-2026-33140.
    """
    def __init__(self, findings):
        self.findings = findings

    def _generate_vulnerable_row(self, finding: ScanFinding) -> str:
        """
        VULNERABLE (pre-0.1.7): Generates a report row without sanitizing input.
        The code snippet is interpolated directly into the HTML.
        """
        return f"""
        <tr>
            <td>{finding.file_path}</td>
            <td>{finding.line_number}</td>
            <td><pre><code>{finding.code_snippet}</code></pre></td>
            <td>{finding.description}</td>
        </tr>
        """

    def _generate_fixed_row(self, finding: ScanFinding) -> str:
        """
        FIXED (0.1.7+): Generates a report row with proper HTML sanitization.
        This function represents the patch for the Stored XSS vulnerability.
        """
        # The core of the fix: use html.escape() to sanitize any data that
        # originated from a scanned file before embedding it in the report.
        # This converts characters like '<', '>', '&' into their HTML-safe
        # equivalents (e.g., '<', '>', '&').
        sanitized_code_snippet = html.escape(finding.code_snippet)
        sanitized_file_path = html.escape(finding.file_path)
        sanitized_description = html.escape(finding.description)

        return f"""
        <tr>
            <td>{sanitized_file_path}</td>
            <td>{finding.line_number}</td>
            <td><pre><code>{sanitized_code_snippet}</code></pre></td>
            <td>{sanitized_description}</td>
        </tr>
        """

    def generate_report(self, use_fix=True):
        """Generates a full HTML report, using either the vulnerable or fixed method."""
        rows = []
        for finding in self.findings:
            if use_fix:
                # This is the patched behavior.
                rows.append(self._generate_fixed_row(finding))
            else:
                # This is the old, vulnerable behavior.
                rows.append(self._generate_vulnerable_row(finding))

        body_content = "\n".join(rows)

        return f"""
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>PySpector Scan Report</title>
            <style>
                body {{ font-family: sans-serif; }}
                table {{ border-collapse: collapse; width: 100%; }}
                th, td {{ border: 1px solid #ccc; padding: 8px; text-align: left; vertical-align: top; }}
                th {{ background-color: #f2f2f2; }}
                pre {{ margin: 0; white-space: pre-wrap; word-wrap: break-word; }}
                code {{ background-color: #eee; padding: 2px 4px; border-radius: 3px; }}
            </style>
        </head>
        <body>
            <h1>PySpector Scan Report</h1>
            <table>
                <thead>
                    <tr>
                        <th>File</th>
                        <th>Line</th>
                        <th>Flagged Code Snippet</th>
                        <th>Description</th>
                    </tr>
                </thead>
                <tbody>
                    {body_content}
                </tbody>
            </table>
        </body>
        </html>
        """

# --- Demonstration ---

if __name__ == '__main__':
    # 1. Define a finding with a malicious JavaScript payload in the code snippet.
    # This simulates PySpector scanning a Python file containing this line.
    xss_payload_snippet = "eval('<script>alert(\"XSS from report!\")</script>')"
    
    malicious_finding = ScanFinding(
        file_path="src/vulnerable_app.py",
        line_number=50,
        code_snippet=xss_payload_snippet,
        description="Dangerous use of 'eval' detected."
    )

    findings_list = [malicious_finding]

    # 2. Generate the report using the FIXED method.
    fixed_generator = ReportGenerator(findings_list)
    fixed_report_html = fixed_generator.generate_report(use_fix=True)

    # 3. Save the report to an HTML file.
    fixed_report_filename = "fixed_report.html"
    with open(fixed_report_filename, "w", encoding="utf-8") as f:
        f.write(fixed_report_html)

    # When you open 'fixed_report.html' in a browser:
    # - The <script> tag will be displayed as plain text.
    # - The JavaScript alert will NOT execute.
    # - The rendered HTML for the malicious code cell will be:
    #   <code>eval('<script>alert("XSS from report!")</script>')</code>
    
    print(f"Generated '{fixed_report_filename}'.")
    print("Open this file in a browser to see that the XSS payload is safely displayed as text.")

Payload

eval("print(1); '<script>alert(\"XSS\")</script>'")

Cite this entry

@misc{vaitp:cve202633140,
  title        = {{PySpector's HTML report is vulnerable to stored XSS via scanned code.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-33140},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-33140/}}
}
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 ::