Skip to content

Dangerous-type forcing must cover all three Content-Type sources on attachment download #66

Description

@cuibonobo

Problem

The download endpoint derives the served Content-Type from three sources (spec §Attachments, "Download"), and the dangerous-type policy is specified for exactly one of them:

Source Forcing specified?
?contentType param Yes — "Dangerous types (HTML, SVG, JS, XML) are forced to application/octet-stream regardless"
Extension inference from ?filename (when ?contentType omitted) No — spec is silent
Stored _attachment@1 mimeType (no params) No — spec is silent

So the hardened path is the one the attacker doesn't have to use: GET /attachments/<fileId>?filename=x.html invites the server to infer text/html with no forcing language covering the result, and a stored metadata record with a scripty type (see #65's escalation scenario) flows to the header equally unforced. XSS-shaped, on the stack's origin — where a web client's bearer token lives.

A server author reading the spec today can implement all three paths "correctly" and ship the hole; that's a spec defect, not an implementation one.

Decided direction

  1. Forcing applies to the result, not the source. Normative spec language: compute the candidate type (?contentType → else extension inference → else stored metadata → else application/octet-stream), then apply the dangerous-type policy to whatever came out. One policy, one application point, impossible to route around by choosing a different source.
  2. Define the policy once, and prefer a safe-list. The current deny-list ("HTML, SVG, JS, XML") is informal and incomplete (e.g. application/xhtml+xml, text/xsl, MIME parameter tricks). Recommended: safe-list — image/* (minus image/svg+xml), video/*, audio/*, application/pdf, text/plain, application/octet-stream; everything else is served as application/octet-stream with Content-Disposition: attachment. At minimum, the deny-list becomes normative and exhaustive-by-category rather than four examples in a table cell.
  3. X-Content-Type-Options: nosniff on every attachment response, mandated by the spec. Without it, browsers may sniff octet-stream responses back into the very types the forcing removed — nosniff is what makes the downgrade stick.
  4. Conformance fixtures (PATCH contract mismatch: spec defines content-only merge patch, APIAdapter sends record-field envelopes #52): dangerous type × each of the three sources → forced; safe type × each source → passes through; nosniff header present.

Relationship to #65: that issue makes the served type deterministic (first-recorded wins, conflicts rejected); this one makes even a deterministic lie inert. Both are needed — determinism without forcing serves the first uploader's text/html faithfully; forcing without determinism leaves caches flapping between safe answers.

Work items

Refs #65 (deterministic type; this is its backstop), #52 (fixtures), #51 (origin-scoped credential threat model context).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions