Skip to content

Derive media content types from file content, not the request - #174

Open
snoopdave wants to merge 1 commit into
masterfrom
media-content-type-handling
Open

Derive media content types from file content, not the request#174
snoopdave wants to merge 1 commit into
masterfrom
media-content-type-handling

Conversation

@snoopdave

Copy link
Copy Markdown
Contributor

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

  • Add MediaTypePolicy as the single place for the stored type, the inline
    allow-list, and the response headers.
  • Derive the stored type from the filename; consult the declared type only for
    opaque names and never adopt an active type (an explicit list plus any +xml
    suffix).
  • Send X-Content-Type-Options: nosniff on every media response.
  • Serve inline only passive images, audio, video, and PDF; serve everything else
    as application/octet-stream with an attachment disposition. SVG is excluded.
  • Route all five upload entry points and the three serving paths through the
    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 every
caller 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 nosniff on every response.

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

@mraible mraible left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed. Centralizing media-type handling is a good direction. Please address the customized-theme regression and the remaining protocol and compatibility notes before merge.

String resourceType = this.context.getMimeType(
resourceRequest.getResourcePath());
if (fromUploadedMedia) {
// Uploaded through the media library, so it is governed by the

@mraible mraible Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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,

@mraible mraible Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@apache apache deleted a comment from mraible Sep 1, 2026
@apache apache deleted a comment from mraible Sep 1, 2026
@apache apache deleted a comment from mraible Sep 1, 2026
@apache apache deleted a comment from mraible Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants