diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index 0c4bf35f45f..bf25b76aa5e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -16,6 +16,7 @@ import java.net.URI; import java.sql.SQLException; import java.util.List; +import java.util.Locale; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -133,7 +134,7 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp Long lastModified = bitstreamService.getLastModified(bit); BitstreamFormat format = bit.getFormat(context); - String mimetype = format.getMIMEType(); + String mimetype = StringUtils.defaultIfBlank(format.getMIMEType(), "application/octet-stream"); String name = getBitstreamName(bit, format); if (StringUtils.isBlank(request.getHeader("Range"))) { @@ -293,7 +294,8 @@ private boolean isNotAnErrorResponse(HttpServletResponse response) { */ private boolean checkFormatForContentDisposition(BitstreamFormat format) { // Undefined or Unknown formats should ALWAYS be downloaded for additional security. - if (format == null || format.getSupportLevel() == BitstreamFormat.UNKNOWN) { + if (format == null || format.getSupportLevel() == BitstreamFormat.UNKNOWN + || StringUtils.isBlank(format.getMIMEType())) { return true; } @@ -306,16 +308,24 @@ private boolean checkFormatForContentDisposition(BitstreamFormat format) { return true; } - // Define a download list of formats which DSpace forces to ALWAYS be downloaded. - // These formats can embed JavaScript which may be run in the user's browser if the file is opened inline. - // Therefore, DSpace blocks opening these formats inline as it could be used for an XSS attack. - List downloadOnlyFormats = List.of("text/html", "text/javascript", "text/xml", "rdf"); + // Compare MIME types without parameters or case differences. + String mimeType = StringUtils.substringBefore(format.getMIMEType(), ";").trim().toLowerCase(Locale.ROOT); + if (mimeType.isEmpty() || mimeType.endsWith("+xml") || mimeType.startsWith("text/javascript")) { + return true; + } + + // Active formats must download regardless of configuration. + List downloadOnlyFormats = List.of("text/html", "text/javascript", "text/xml", "application/xml", + "rdf", "application/javascript", "application/ecmascript", "text/ecmascript", + "application/x-javascript", "application/x-ecmascript", "text/x-javascript", "text/x-ecmascript", + "text/jscript", "text/livescript"); // Combine our two lists List formats = ListUtils.union(downloadOnlyFormats, configuredFormats); // See if the passed in format's MIME type or file extension is listed. - boolean download = formats.contains(format.getMIMEType()); + boolean download = formats.stream().anyMatch(value -> + mimeType.equalsIgnoreCase(StringUtils.substringBefore(value, ";").trim())); if (!download) { for (String ext : format.getExtensions()) { if (formats.contains(ext)) { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 7e07c707216..6b0086a72ad 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -1432,6 +1432,68 @@ public void checkWildcardContentDispositionFormats() throws Exception { } + @Test + public void checkActiveContentFormatsNeverServedInline() throws Exception { + String[] mimeTypes = { + "image/svg+xml", "application/xhtml+xml", "image/SVG+XML", "APPLICATION/XHTML+XML", + "text/html; charset=utf-8", "application/javascript", "application/ecmascript", + "text/ecmascript", "application/x-javascript", "application/x-ecmascript", + "text/x-javascript", "text/x-ecmascript", "text/jscript", "text/livescript", "text/javascript1.5" + }; + configurationService.setProperty("webui.content_disposition_format", new String[0]); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + context.restoreAuthSystemState(); + + for (String mimeType : mimeTypes) { + context.turnOffAuthorisationSystem(); + // Exercise registered active formats, not the unknown-format fallback. + createBitstreamFormat(context).withMimeType(mimeType) + .withShortDescription("Active " + mimeType).withSupportLevel(KNOWN).build(); + item = context.reloadEntity(item); + Bitstream file = BitstreamBuilder.createBitstream(context, item, toInputStream("test", UTF_8)) + .withMimeType(mimeType).build(); + context.restoreAuthSystemState(); + + String contentType = mimeType.split(";")[0] + ";charset=UTF-8"; + configurationService.setProperty("webui.content_disposition_threshold", 8388608); + verifyBitstreamDownload(file, contentType, true); + configurationService.setProperty("webui.content_disposition_threshold", -1); + verifyBitstreamDownload(file, contentType, true); + } + } + + @Test + public void checkInlineDispositionSizeLimit() throws Exception { + context.turnOffAuthorisationSystem(); + bitstream.setFormat(context, bitstreamFormatService.findByMIMEType(context, "application/pdf")); + bitstreamService.update(context, bitstream); + context.restoreAuthSystemState(); + + configurationService.setProperty("webui.content_disposition_threshold", bitstream.getSizeBytes()); + verifyBitstreamDownload(bitstream, "application/pdf;charset=UTF-8", false); + configurationService.setProperty("webui.content_disposition_threshold", bitstream.getSizeBytes() - 1); + verifyBitstreamDownload(bitstream, "application/pdf;charset=UTF-8", true); + configurationService.setProperty("webui.content_disposition_threshold", -1); + verifyBitstreamDownload(bitstream, "application/pdf;charset=UTF-8", false); + } + + @Test + public void checkMissingMimeTypeDownloads() throws Exception { + configurationService.setProperty("webui.content_disposition_format", new String[0]); + for (String mimeType : new String[] {null, "", " "}) { + context.turnOffAuthorisationSystem(); + supportedFormat = context.reloadEntity(supportedFormat); + supportedFormat.setMIMEType(mimeType); + bitstreamFormatService.update(context, supportedFormat); + context.restoreAuthSystemState(); + verifyBitstreamDownload(bitstream, "application/octet-stream;charset=UTF-8", true); + } + } + private void verifyBitstreamDownload(Bitstream file, String contentType, boolean shouldDownload) throws Exception { String token = getAuthToken(admin.getEmail(), password); String header = getClient(token).perform(get("/api/core/bitstreams/" + file.getID() + "/content")