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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ It supports the [AWS, Azure, and Google object stores](storage-targets/cds-featu
* [Malware Scanner](#malware-scanner)
* [Specify the maximum file size](#specify-the-maximum-file-size)
* [Restrict allowed MIME types](#restrict-allowed-mime-types)
* [Content Disposition](#content-disposition)
* [Outbox](#outbox)
* [Restore Endpoint](#restore-endpoint)
* [Motivation](#motivation)
Expand Down Expand Up @@ -249,6 +250,21 @@ annotate Books.attachments with {
}
```

### Content Disposition

Attachment content is served with `Content-Disposition: attachment`. Browsers therefore download the file instead of rendering it inline in the application origin. This is the safe default that mitigates stored XSS attacks via user-uploaded HTML or SVG payloads, because the content type of a stored attachment is derived from the uploaded file.

If your application intentionally wants to preview certain attachments inline (e.g. images restricted via `@Core.AcceptableMediaTypes`), you can opt in explicitly in your own CDS model:

```cds
using { sap.attachments.Attachments } from '@cap-js/cds-feature-attachments';

annotate Books.attachments with {
content @Core.ContentDisposition.Type: 'inline';
}
```

> **Security warning:** Only enable inline disposition for MIME types that cannot execute script — for example, plain images such as `image/jpeg` or `image/png` — restricted via `@Core.AcceptableMediaTypes`. Never enable inline disposition for `image/svg+xml`, `text/html`, `application/xhtml+xml`, or `application/xml` when the uploader is not fully trusted. Note that `application/pdf` may execute JavaScript in some PDF viewers (notably Adobe Reader); verify the viewer used by your target audience before allowing PDFs inline.

### Outbox

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ annotate sap.attachments.MediaData with @UI.MediaResource: {Stream: content} {
annotate sap.attachments.Attachments {
content @(
Core.ContentDisposition: {
Filename: fileName,
Type : 'inline',
Filename: fileName
},
Core.MediaType : mimeType
);
Expand Down
6 changes: 6 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).

The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Unreleased

### Security

- Attachment content is now served with `Content-Disposition: attachment` by default (previously `inline`) to mitigate stored XSS (CWE-79) via user-uploaded SVG/HTML payloads (cds-calesi #1263). Applications that require inline previews can opt in by annotating `content` in their own CDS model — see the "Content Disposition" section in the README. It is recommended to combine this with `@Core.AcceptableMediaTypes` restricting inline content to non-scriptable types (e.g. `image/jpeg`, `image/png`, `application/pdf`).

## Version 1.6.0 - 2026-07-17

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* © 2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors.
* © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors.
*/
package com.sap.cds.feature.attachments.integrationtests.nondraftservice;

Expand All @@ -11,9 +11,11 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MvcResult;

@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED)
class OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest
Expand All @@ -24,6 +26,49 @@ void serviceHandlerIsNull() {
assertThat(serviceHandler).isNull();
}

/**
* Regression test for the stored XSS mitigation (see cds-calesi #1263). Content served for
* attachments must default to {@code Content-Disposition: attachment} so that browsers download
* the file rather than render it inline in the application origin. Applications that require
* inline previews can opt in by annotating {@code content} in their own CDS model.
*/
@Test
void contentDownloadUsesAttachmentDisposition() throws Exception {
var serviceRoot = buildServiceRootWithDeepData();
postServiceRoot(serviceRoot);

var selectedRoot = selectStoredRootWithDeepData();
var item = getItemWithAttachment(selectedRoot);
var itemAttachment = getRandomItemAttachment(item);
putContentForAttachmentWithNavigation(selectedRoot, itemAttachment);
clearServiceHandlerContext();

var contentUrl =
buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId())
+ "/content";

// Wait for the (async) malware scan to complete and content to become readable,
// capturing the successful response for assertion (avoids a redundant second GET).
var responseRef = new AtomicReference<MvcResult>();
Awaitility.await()
.atMost(10, TimeUnit.SECONDS)
.until(
() -> {
var r = requestHelper.executeGet(contentUrl);
if (r.getResponse().getStatus() == 200) {
responseRef.set(r);
return true;
}
return false;
});

var contentDisposition = responseRef.get().getResponse().getHeader("Content-Disposition");
assertThat(contentDisposition)
.as("Content-Disposition must default to 'attachment' (see cds-calesi #1263)")
.isNotNull()
.startsWith("attachment;");
}

@Override
protected void executeContentRequestAndValidateContent(String url, String content)
throws Exception {
Expand Down
Loading