From 2cf9fd0dba761366b4bf4830d43db4378b2b16ba Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 21 Jul 2026 12:04:11 +0200 Subject: [PATCH 1/2] fix: validate supplied mimeType against acceptable media types The acceptable-media-types check only resolved the MIME type from the uploaded file name. The mimeType value supplied in the payload was persisted and served inline (Core.MediaType + inline ContentDisposition) without being validated against @Core.AcceptableMediaTypes. An allowed file name (e.g. report.pdf) could therefore be paired with a disallowed MIME type (e.g. text/html), enabling stored active content. Validation now additionally checks the explicitly supplied mimeType values against the acceptable media types. --- .../AttachmentDataExtractor.java | 31 ++++++++++++ .../AttachmentValidationHelper.java | 48 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentDataExtractor.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentDataExtractor.java index 1250c034a..6c73e0180 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentDataExtractor.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentDataExtractor.java @@ -23,8 +23,11 @@ public final class AttachmentDataExtractor { private static final String FILE_NAME_FIELD = "fileName"; + private static final String MIME_TYPE_FIELD = "mimeType"; public static final Filter FILE_NAME_FILTER = (path, element, type) -> element.getName().contentEquals(FILE_NAME_FIELD); + public static final Filter MIME_TYPE_FILTER = + (path, element, type) -> element.getName().contentEquals(MIME_TYPE_FIELD); /** * Extracts and validates file names of attachments from the given entity data. @@ -42,6 +45,34 @@ public static Map> extractAndValidateFileNamesByElement( return fileNamesByElementName; } + /** + * Extracts the explicitly supplied {@code mimeType} values of attachments from the given entity + * data, keyed by the qualified name of the declaring media entity. + * + *

Unlike file names, these values are not derived from the file extension but taken verbatim + * from the request payload (or, later, HTTP headers). They must therefore be validated against + * the acceptable media types as well, otherwise an allowed file name could be paired with a + * disallowed MIME type that is subsequently persisted and served inline. + * + * @param entity the CDS entity definition + * @param data the incoming data containing attachment values + * @return a map of declaring media entity names to sets of supplied MIME types + */ + public static Map> extractMimeTypesByElement( + CdsEntity entity, List data) { + Map> mimeTypesByElementName = new HashMap<>(); + CdsDataProcessor processor = CdsDataProcessor.create(); + Validator mimeTypeValidator = + (path, element, value) -> { + if (value instanceof String mimeType && !mimeType.isBlank()) { + String key = element.getDeclaringType().getQualifiedName(); + mimeTypesByElementName.computeIfAbsent(key, k -> new HashSet<>()).add(mimeType.trim()); + } + }; + processor.addValidator(MIME_TYPE_FILTER, mimeTypeValidator).process(data, entity); + return mimeTypesByElementName; + } + private static Map> collectFileNamesByElementName( CdsEntity entity, List data) { // Use CdsProcessor to traverse the data and collect file names for elements diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentValidationHelper.java index 72df1920b..a998dbdce 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/mimeTypeValidation/AttachmentValidationHelper.java @@ -60,6 +60,54 @@ public static void validateMediaAttachments( Map> fileNamesByElementName = AttachmentDataExtractor.extractAndValidateFileNamesByElement(entity, data); validateAttachmentMediaTypes(fileNamesByElementName, allowedTypesByElementName); + + // validate the explicitly supplied mime types as well. The file name check alone can be + // bypassed by pairing an allowed file name (e.g. "report.pdf") with a disallowed MIME type + // (e.g. "text/html") that is persisted and served inline. + Map> mimeTypesByElementName = + AttachmentDataExtractor.extractMimeTypesByElement(entity, data); + validateAttachmentMimeTypes(mimeTypesByElementName, allowedTypesByElementName); + } + + private static void validateAttachmentMimeTypes( + Map> mimeTypesByElementName, + Map> acceptableMediaTypesByElementName) { + + Map> invalidMimeTypes = + findInvalidMimeTypesByElementName( + mimeTypesByElementName, acceptableMediaTypesByElementName); + + if (!invalidMimeTypes.isEmpty()) { + throw buildUnsupportedFileTypeMessage(acceptableMediaTypesByElementName, invalidMimeTypes); + } + } + + private static Map> findInvalidMimeTypesByElementName( + Map> mimeTypesByElementName, + Map> acceptableMediaTypesByElementName) { + if (mimeTypesByElementName == null || mimeTypesByElementName.isEmpty()) { + return Map.of(); + } + Map> invalidMimeTypes = new HashMap<>(); + mimeTypesByElementName.forEach( + (elementName, mimeTypes) -> { + List acceptableTypes = acceptableMediaTypesByElementName.get(elementName); + if (acceptableTypes == null) { + return; + } + + List invalid = + mimeTypes.stream() + .filter( + mimeType -> !MediaTypeService.isMimeTypeAllowed(acceptableTypes, mimeType)) + .toList(); + + if (!invalid.isEmpty()) { + invalidMimeTypes.put(elementName, invalid); + } + }); + + return invalidMimeTypes; } private static void validateAttachmentMediaTypes( From 43035d2ba18f031db3df0d6186cb13a0fa25f8e8 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 21 Jul 2026 12:06:03 +0200 Subject: [PATCH 2/2] test: cover filename/mimeType mismatch media validation bypass Adds integration tests asserting that an allowed file name paired with a disallowed mimeType is rejected (415), and that a matching allowed mimeType is still accepted. --- ...MediaValidatedAttachmentsNonDraftTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java index cc8c2e099..db5f8f714 100644 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -82,6 +82,28 @@ void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception { status().isBadRequest()); } + @Test + void shouldRejectAttachment_whenAllowedFileNameCarriesDisallowedMimeType() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = + objectMapper.writeValueAsString(Map.of("fileName", "image.jpg", "mimeType", "text/html")); + + requestHelper.assertPostStatus( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isUnsupportedMediaType()); + } + + @Test + void shouldAcceptAttachment_whenAllowedFileNameAndAllowedMimeType() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = + objectMapper.writeValueAsString(Map.of("fileName", "image.jpg", "mimeType", "image/jpeg")); + + requestHelper.assertPostStatus( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), attachmentMetadata, status().isCreated()); + } + @Test void shouldAcceptUppercaseExtension_whenMimeTypeIsAllowed() throws Exception { String rootId = createRootAndReturnId();