Skip to content

fix(backend): validate uploaded images with libmagic#3353

Merged
gantoine merged 3 commits into
masterfrom
validate-image-uploads
May 9, 2026
Merged

fix(backend): validate uploaded images with libmagic#3353
gantoine merged 3 commits into
masterfrom
validate-image-uploads

Conversation

@gantoine
Copy link
Copy Markdown
Member

@gantoine gantoine commented May 9, 2026

Summary

  • Adds a shared validate_image_upload() helper in assets_handler.py that sniffs the leading bytes of an upload with libmagic and returns the trusted file extension, raising HTTPException(400) if the file is not in SAFE_IMAGE_MIME_TYPES (PNG, JPEG, WebP, GIF).
  • Applies the helper to all four image-upload sites: avatar (update_user), ROM artwork (update_rom), and collection artwork (add_collection / update_collection). Files are saved with the extension derived from the detected MIME type, not from the user-supplied filename.
  • Pairs with the existing change in raw.py that decides between inline and attachment content disposition based on the on-disk extension — this PR ensures that extension reflects the real file type, so a malicious filename like payload.html can no longer be served back as HTML.

Previously the artwork endpoints relied on Pillow to fail closed on unrecognized formats; this is defense-in-depth and gives a clear 400 instead of a silent (None, None) return.

Test plan

  • Existing test_update_user_rejects_non_image_avatar and test_update_user_accepts_png_avatar still pass.
  • update_rom with a non-image file (e.g. HTML payload renamed cover.png) returns 400.
  • add_collection / update_collection with a real PNG/JPEG/WebP/GIF still saves correctly and serves inline via /raw/assets/....
  • Local DB-backed pytest suite runs green (skipped here due to local romm_test MariaDB grant issue unrelated to this change).

🤖 Generated with Claude Code

Avatar, ROM artwork, and collection artwork uploads now sniff the file
header with libmagic and reject anything that isn't PNG/JPEG/WebP/GIF,
saving the file with an extension derived from the detected MIME rather
than the user-supplied filename. Pairs with the raw asset endpoint,
which decides inline vs attachment from the on-disk extension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 13:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens backend asset handling by validating uploaded “image” files via libmagic and by ensuring the raw asset endpoint only serves trusted image types inline (everything else as an attachment) to mitigate stored XSS risks.

Changes:

  • Added validate_image_upload() + SAFE_IMAGE_MIME_TYPES to sniff uploads and derive a trusted extension.
  • Applied image sniffing to avatar, ROM artwork, and collection artwork upload flows (extension no longer taken from user filename).
  • Updated /raw/assets/... responses to serve only trusted image MIME types inline; others as application/octet-stream attachments, with tests updated accordingly.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
backend/handler/filesystem/assets_handler.py Adds shared libmagic-based image upload validation + safe MIME/extension mapping.
backend/endpoints/user.py Uses validation helper for avatar uploads and enforces trusted extension.
backend/endpoints/roms/init.py Uses validation helper for ROM artwork uploads.
backend/endpoints/collections.py Uses validation helper for collection artwork uploads (add/update).
backend/endpoints/raw.py Builds FileResponse with inline vs attachment behavior based on safe image MIME types.
backend/tests/endpoints/test_raw.py Updates assertions to match new attachment behavior for non-image assets.
backend/tests/endpoints/test_identity.py Adds avatar upload tests for rejecting non-images and accepting valid PNG with filename spoofing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/endpoints/raw.py Outdated
Comment thread backend/endpoints/raw.py Outdated
Comment thread backend/endpoints/roms/__init__.py
Comment thread backend/endpoints/collections.py
Comment thread backend/endpoints/collections.py
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

Test Results (mariadb)

    1 files      1 suites   4m 3s ⏱️
1 294 tests 1 294 ✅ 0 💤 0 ❌
1 296 runs  1 296 ✅ 0 💤 0 ❌

Results for commit e3aaa10.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
16187 11148 69% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
backend/endpoints/collections.py 61% 🟢
backend/endpoints/raw.py 67% 🟢
backend/endpoints/roms/_init_.py 63% 🟢
backend/endpoints/user.py 61% 🟢
backend/handler/filesystem/assets_handler.py 73% 🟢
TOTAL 65% 🟢

updated for commit: e3aaa10 by action🐍

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

Test Results (postgresql)

    1 files      1 suites   3m 27s ⏱️
1 294 tests 1 294 ✅ 0 💤 0 ❌
1 296 runs  1 296 ✅ 0 💤 0 ❌

Results for commit e3aaa10.

♻️ This comment has been updated with latest results.

Adds rejection + acceptance tests for update_rom, add_collection, and
update_collection artwork uploads, mirroring the existing avatar tests:
non-image content returns 400, and a real PNG uploaded under a misleading
filename like payload.html is stored with the trusted .png extension.

Also fixes two `return HTTPException(...)` → `raise` in raw.py so the 404
path actually surfaces instead of silently returning the exception object.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread backend/handler/filesystem/assets_handler.py
Comment thread backend/endpoints/raw.py
magic.Magic(mime=True) loads the magic database from disk on construction;
instantiating it per request was adding pointless overhead to every avatar
and artwork upload. Share a module-level instance guarded by a lock (the
underlying magic_t handle is not thread-safe), and surface MagicException
as a 400 so a sniffing failure fails closed instead of bubbling a 500.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gantoine gantoine merged commit f3b46d7 into master May 9, 2026
12 checks passed
@gantoine gantoine deleted the validate-image-uploads branch May 9, 2026 14:35
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.

2 participants