Skip to content

fix(security): force download of active-content bitstreams - #1541

Merged
milanmajchrak merged 4 commits into
dtq-devfrom
sec/inline-disposition-xss
Oct 28, 2026
Merged

milanmajchrak merged 4 commits into
dtq-devfrom
sec/inline-disposition-xss

Conversation

@Kasinhou

@Kasinhou Kasinhou commented Oct 27, 2026 •

Copy link
Copy Markdown

Problem

Registered SVG/XHTML bitstreams could be served inline by the /content endpoint and execute JavaScript on the serving origin. The existing forced-download list did not cover these XML-based types or MIME values with different casing/parameters.

Part of dataquest-dev/dspace-customers#1104 (R35), see #1083.

Fix

Extend the existing forced-download check to cover XML/+xml and JavaScript MIME variants, comparing MIME types without case differences or parameters. Missing MIME types download as application/octet-stream.

This keeps the existing denylist configuration, method names, wildcard behavior, size threshold and inline handling for other formats. There are no configuration-file changes or allowlist migration.

Validation

Regression tests cover registered SVG/XHTML, mixed-case MIME types, HTML with charset parameters, JavaScript variants, missing MIME types and the existing threshold boundary/-1 behavior. Existing disposition tests are retained unchanged.

The previous CI unit-test failure was a duplicate DOI in DOIIdentifierProviderTest.testDelete_all_DOIs, in unchanged dspace-api code. A local rerun of that full test class passed all 27 tests. No unrelated DOI changes are included.

Local validation: production/test compilation passed, all 6 targeted content-disposition integration tests passed (0 failures/errors), and Checkstyle passed. Full GitHub CI must pass on the updated commit before merge.

Switch the /content bitstream download disposition decision from a
denylist (default = inline) to an allowlist (default = attachment).
A bitstream is now served inline only when its format is listed in the
new webui.content_disposition_inline config and its size is within the
inline threshold; everything else is forced to download.

An additional guard forces attachment for any MIME type ending in
"+xml" (e.g. image/svg+xml, application/xhtml+xml), which can carry
executable script, even if a deployment adds such a type to the
allowlist. Because the controller now always sets an explicit
disposition, the image/* content type no longer bypasses the
Content-Disposition header and active-content images download instead
of rendering inline.

Add webui.content_disposition_inline to dspace.cfg and update the
integration tests for the allowlist behavior, including cases asserting
that image/svg+xml and application/xhtml+xml are served as attachments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Two critical and two moderate issues remain unresolved in the disposition guard and MIME handling.

3 open issues
What changed in this PR

This pull request hardens bitstream downloads against stored XSS by replacing default-inline behavior with a configurable inline MIME-type allowlist and attachment-by-default handling.

Changes:

  • Adds webui.content_disposition_inline with safe default MIME types and revised threshold semantics.
  • Applies allowlist, size-threshold, and active-content protections to Content-Disposition.
  • Updates integration tests for allowlisted formats, default attachments, and SVG/XHTML protection.
File Summary Review findings
dspace/​config/​dspace.cfg Defines the inline MIME-type allowlist and disposition threshold configuration. No findings.
dspace-server-webapp/​src/​test/​java/​org/​dspace/​app/​rest/​BitstreamRestControllerIT.java Updates disposition tests and adds coverage for allowlisted and active-content formats. Moderate (1 vote): shared configuration changes are not restored, creating order-dependent tests. Nit (4 votes): add coverage for formats above the threshold and for a -1 threshold.
dspace-server-webapp/​src/​main/​java/​org/​dspace/​app/​rest/​BitstreamRestController.java Implements attachment-by-default disposition handling with allowlist, threshold, and banned-format checks. Critical (1 vote): the +xml guard is case-sensitive. Critical (1 vote): standard JavaScript MIME types such as application/javascript are not blocked. Moderate (1 vote): nullable MIME types can cause a download to fail with an NPE.

Review effort: Lite

@Kasinhou Kasinhou changed the title fix(security): default-attachment allowlist for bitstream downloads (stored XSS) fix(security): force download of active-content bitstreams Oct 27, 2026
@Kasinhou
Kasinhou requested review from milanmajchrak and a lite review from Copilot October 27, 2026 14:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The security behavior is covered and no approval-blocking issues remain; the remaining configured-MIME test enhancement is a minor nit.

No open issues 🎉

3 resolved since last review

Review effort: Lite

@milanmajchrak
milanmajchrak merged commit bc5c925 into dtq-dev Oct 28, 2026
13 checks passed
@milanmajchrak

Copy link
Copy Markdown
Collaborator

Why force download

A user who can upload a file can set its format to an active-content type and get /content to
serve it inline. The browser opens it instead of downloading, and embedded script runs on the
repository origin as the user who clicked — stored XSS.

It also works with the built-in CC License format: its MIME is text/html; charset=utf-8, and
the old denylist compared MIME strings exactly, so the ; charset=utf-8 slipped through.

flowchart LR
  A[Upload HTML or SVG, set active format] --> B[Victim clicks the file]
  B --> C{/content disposition}
  C -->|before: inline| D[Opens, script runs as victim]
  C -->|after: attachment| E[Only downloads, nothing runs]
Loading
File Format MIME Before After
HTML as "CC License" text/html; charset=utf-8 opens, script runs downloads
SVG / XHTML * image/svg+xml, application/xhtml+xml opens downloads
application/xml, mixed case * application/xml, image/SVG+XML opens downloads
No MIME * empty undefined downloads (octet-stream)
PDF / images ≤ 8 MB — opens unchanged

The fix compares MIME without parameters and case-insensitively, and forces download for +xml,
text/javascript*, application/xml and JS aliases. Threshold, *, extension matching and inline
handling for other formats stay as they were.

* only if such a format is in the registry. The default registry has none of these, so they land as
Unknown and already download. The CC License row is the one that works on every instance.

Standalone alternative to the 7.6.7 upgrade PR (1542), which fixes the same thing with an inline
allowlist and conflicts in the same method — only one is needed.

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.

3 participants