Skip to content

[Security][Level 3] app.py: /api/download accepts arbitrary path parameter, allowing full filesystem read via path traversal #286

Description

@anshul23102

Summary

The /api/download endpoint in app.py takes a path query parameter and passes it directly to os.path.isfile and send_from_directory with no validation. An unauthenticated attacker can read any file on the server that the Flask process has permission to access, including application secrets, environment files, and OS files.

Affected file

app.py, lines ~196-202

Root cause

@app.route('/api/download')
def download():
    path = request.args.get('path')
    if not path or not os.path.isfile(path.lstrip('/')):   # no containment check
        return 'File not found', 404
    dir_name = os.path.dirname(path.lstrip('/'))
    file_name = os.path.basename(path)
    return send_from_directory(dir_name, file_name, as_attachment=True)

path.lstrip('/') removes the leading slash but does not prevent traversal. send_from_directory(dir_name, file_name) with an attacker-controlled dir_name will serve any reachable file.

Proof of concept

# Read the .env file (contains GROQ_API_KEY and other secrets)
curl "http://localhost:5000/api/download?path=/.env"

# Read /etc/passwd on Linux
curl "http://localhost:5000/api/download?path=/etc/passwd"

# Traverse to parent directories
curl "http://localhost:5000/api/download?path=../../.env"

The downloadUrl stored in notes-data.json uses the same pattern, so the surface is already in production use.

Impact

  • Credential exposure: .env contains GROQ_API_KEY; reading it lets an attacker exhaust the project's Groq quota or sell the key.
  • Source code and data theft: data/notes-data.json, utils/chatbot.py, app.py itself, and any other application file can be downloaded.

Suggested fix

Restrict downloads to a known safe root directory using pathlib.Path.resolve():

from pathlib import Path

DOWNLOAD_ROOT = Path(UPLOAD_ROOT).resolve()

@app.route('/api/download')
def download():
    raw_path = request.args.get('path', '')
    try:
        safe_path = (DOWNLOAD_ROOT / raw_path.lstrip('/')).resolve()
        safe_path.relative_to(DOWNLOAD_ROOT)   # raises ValueError if outside root
    except (ValueError, Exception):
        return 'Forbidden', 403
    if not safe_path.is_file():
        return 'File not found', 404
    return send_from_directory(safe_path.parent, safe_path.name, as_attachment=True)

Level

Level 3 - unauthenticated path traversal enabling arbitrary file read on the server.

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