feat(feedback): magic-byte MIME validation + dim bomb cap + proxy hardening #112
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
psa-systems/bunyip!112
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/feedback-attachment-security-hardening"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BUNYIP-90. Stacks on BUNYIP-86.
BUNYIP-86 shipped attachment uploads with MIME validation that trusted the browser-sent
Content-Typefield. An attacker could uploadevil.exewithContent-Type: image/pngand the server stored it as image/png. Several adjacent gaps made the surface larger than necessary: cross-type substitution (real GIF claiming image/png), decompression bombs (5 MB "PNG" that decodes to a billion pixels and explodes the admin browser), filename injection (control characters that break CSV export and logging), and browser MIME sniffing on the admin proxy response.Server-side hardening in
bunyip-api/src/handlers/feedback.rs:infer = "0.16"(zero-dep magic-byte sniffer) andimagesize = "0.13"(header-only image dimension reader; does NOT decode pixels). Both tiny.derive_canonical_mime(bytes, declared): sniffs the magic bytes viainfer::get. If recognised, the sniffed MIME must be in the allowlist and OVERRIDES whatever the browser sent (browser claim ignored). If unrecognised (no magic bytes), the only accepted shape is text/plain claimed + bytes valid as UTF-8; everything else rejects. A binary masquerading as text/plain fails the UTF-8 check; a GIF claiming image/png is accepted but stored as image/gif (sniffer wins).validate_filename(name): basename-strip + length cap at 200 chars + reject any control character (NULL, newline, tab, etc.) that would break logging, CSV, and downstream callers.enforce_image_dimensions(mime, bytes): forimage/*MIMEs, reads dimensions viaimagesize::blob_size(header-only parse, no decoder instantiated against adversarial input). Rejects width or height > 10000 px. Defends against decompression bombs the byte-cap can not catch.The three helpers replace the inline declared-MIME check at
submit_feedback. Every check now runs against file CONTENT, never the browser's claim.Admin BFF proxy hardening in
bunyip-web/src/handlers/admin.rs:with_attachment_hardening(builder)helper applied to bothfeedback_attachment(binary download) andfeedback_export(CSV) so future binary-serving routes get the same treatment by reference.X-Content-Type-Options: nosniffforces the browser to respect our declared Content-Type and skip its own MIME sniffing. A text/plain attachment that contains HTML-ish markup never becomes HTML even on legacy browsers.Content-Security-Policy: sandboxsandboxes any inline-rendered content (strictest sandbox: no scripts, no forms, no same-origin). Defence in depth alongside nosniff.Referrer-Policy: no-referrerprevents the attachment URL (feedback id + attachment id) from leaking via Referer when the admin navigates elsewhere.Out of scope for this PR (intentionally deferred):
just check-containerclean (modulo the pre-existingtest_config_defaultsparallel-env-var flake onmain; 188 other tests pass; fmt + clippy + build clean).#BUNYIP-90