Repository navigation
fix(security): force download of active-content bitstreams - #1541
Conversation
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.
There was a problem hiding this comment.
🟡 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_inlinewith 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
Why force downloadA user who can upload a file can set its format to an active-content type and get It also works with the built-in CC License format: its MIME is 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]
The fix compares MIME without parameters and case-insensitively, and forces download for * only if such a format is in the registry. The default registry has none of these, so they land as Standalone alternative to the 7.6.7 upgrade PR (1542), which fixes the same thing with an inline |


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.