Skip to content

fix(uploads): cap attachment size and refuse browser-executable MIME types#42

Merged
jamofer merged 1 commit into
masterfrom
fix/uploads-size-mime-cap
May 24, 2026
Merged

fix(uploads): cap attachment size and refuse browser-executable MIME types#42
jamofer merged 1 commit into
masterfrom
fix/uploads-size-mime-cap

Conversation

@jamofer

@jamofer jamofer commented May 24, 2026

Copy link
Copy Markdown
Owner

Summary

Attachment endpoints accepted any file of any size: shutil.copyfileobj(file.file, dest) streamed straight to disk with no upper bound and no content-type validation. Two real failure modes:

  • Disk-fill DoS: any authenticated user could push gigabytes per request. With no cap the only limit was uvicorn's request body timeout.
  • Stored XSS / drive-by: an HTML or SVG-with-script attachment served back through the download endpoint renders inline in the victim's browser. The download handler does set a filename header but the browser still honours Content-Type: text/html for inline rendering, so the attack worked end to end.

Fix:

  • Two new env-tunable settings in core/config.py:
    • MAX_ATTACHMENT_BYTES (default 25 MiB) — generous enough for screenshots, junit bundles, short screen recordings; well below disk-fill territory.
    • BLOCKED_ATTACHMENT_MIME_TYPES (CSV) — defaults block text/html, application/xhtml+xml, application/javascript, text/javascript, image/svg+xml, application/x-msdownload, application/x-httpd-php, text/x-php. Parsed on demand via a blocked_attachment_mime_types property so admins can override per deployment.
  • New services/uploads.read_attachment(file) validates the MIME (415 on hit) then drains bytes through the existing read_bounded helper (413 on overflow). Returns the bytes for the caller to persist.
  • Wired into all five attachment handlers — bug, test case, execution, result, and version — each replacing the old shutil.copyfileobj + os.path.getsize pattern with payload = uploads.read_attachment(file) + len(payload).

Tests (backend/tests/security/test_upload_caps.py, parametrized × 5 endpoints):

  • Oversize payload above a monkeypatched cap returns 413 on every endpoint.
  • text/html body returns 415 with the offending MIME echoed in the detail.
  • image/svg+xml body returns 415 (covers the script-bearing SVG vector specifically).
  • Allowed text/plain payload still succeeds with 201.

Full backend suite: 894 passing.

@jamofer jamofer merged commit 4c20208 into master May 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant