Derive media content types from file content, not the request - #174
Derive media content types from file content, not the request#174snoopdave wants to merge 1 commit into
Conversation
The type an upload declares is treated as a hint, and the stored type is derived from the file name through one shared MediaTypePolicy. Serving applies the other half of the policy: only a short list of passively-rendered formats is sent inline, everything else is sent as an attachment, and every media response carries nosniff. All paths that accept an upload and all paths that serve uploaded media route through the policy, including the entry editor's replacement-body path and the resource servlets' uploaded-media fallback. Files whose type is outside the inline list, such as CSS and JavaScript held as media, now download rather than render. That is a behaviour change and belongs in the release notes. Claude-Session: https://claude.ai/code/session_01A1fhY1E2PCFU6UAPXu2WtV
| String resourceType = this.context.getMimeType( | ||
| resourceRequest.getResourcePath()); | ||
| if (fromUploadedMedia) { | ||
| // Uploaded through the media library, so it is governed by the |
There was a problem hiding this comment.
This branch is also where customized-theme resources land: WeblogCustomTheme.getResource() looks up the media file but never assigns it to resource, so it always returns null, and importTheme stores theme CSS/JS as media files with their original path. resourceType for css/bootstrap.css is text/css, which isn't inline-safe, so the stylesheet goes out as an octet-stream attachment with nosniff and the browser drops it. Fixing WeblogCustomTheme.getResource to return the media file would route these through the theme branch above; alternatively, treat text/css / text/javascript from the servlet-context mime table as theme-authored here as the description already promises.
| // Replacing the body re-decides the type, on the same | ||
| // terms as the original upload. | ||
| mediaFile.setContentType(MediaTypePolicy.storedTypeFor( | ||
| mediaFile.getName(), this.uploadedFileContentType)); |
There was a problem hiding this comment.
On replace this derives the type from the record's (old) name rather than the uploaded replacement's name (this.uploadedFileName is right there). Replacing photo.jpg with photo.png without renaming stores image/jpeg for PNG bytes and serves them with nosniff; replacing clip.txt with a video keeps text/plain and forces a download. putMedia in MediaCollection has the same mismatch with mf.getName().
| } | ||
| try { | ||
| return Utilities.getContentTypeFromFileName(fileName); | ||
| } catch (Exception undetermined) { |
There was a problem hiding this comment.
Utilities.getContentTypeFromFileName is backed by javax.activation's default map, which knows about 22 extensions (no pdf, svg, webp, mp4, mp3, zip, css, js). So "the name decides" only holds for those; for everything else the declared type is stored after all (a report.pdf declared application/zip is stored as zip and downloads). The servlets already use the servlet context's mime table (web.xml has a full one); the policy should consult the same table.
| if (fromUploadedMedia) { | ||
| // Uploaded through the media library, so it is governed by the | ||
| // same policy as any other media response. | ||
| MediaTypePolicy.applyResponseHeaders(response, resourceType, |
There was a problem hiding this comment.
context.getMimeType() returns null for extensions not mapped in web.xml/the container, and applyResponseHeaders turns null into an octet-stream attachment. On master setContentType(null) left the type unset and the browser could still display the file; now any unmapped (or uppercase, on a case-sensitive container) extension downloads in the theme preview.
This change derives a stored media file's content type from the file itself
rather than from the type declared with the upload, and serves media inline only
for a small explicit allow-list of passive formats.
What changed
MediaTypePolicyas the single place for the stored type, the inlineallow-list, and the response headers.
opaque names and never adopt an active type (an explicit list plus any
+xmlsuffix).
X-Content-Type-Options: nosniffon every media response.as
application/octet-streamwith an attachment disposition. SVG is excluded.policy.
Note for the release notes: media held as CSS or JavaScript now downloads
instead of loading inline — a user-visible compatibility change.
Tests
MediaTypePolicyTest(9 behavioral cases plus 3 source audits asserting everycaller routes through the policy) covers a file whose declared type does not
match its name, SVG / XHTML and unknown types, genuine images retaining their
type, and
nosniffon every response.