Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"))) {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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<String> 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<String> 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<String> 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading