Authenticated Stored Cross-Site Scripting (XSS) in Multiple WSO2 Products via API Document Upload in Publisher
Description
An authenticated stored cross-site scripting (XSS) vulnerability exists in multiple WSO2 products due to improper validation of user-supplied input during API document upload in the Publisher portal. A user with publisher privileges can upload a crafted API document containing malicious JavaScript, which is later rendered in the browser when accessed by other users.
A successful attack could result in redirection to malicious websites, unauthorized UI modifications, or exfiltration of browser-accessible data. However, session-related sensitive cookies are protected by the httpOnly flag, preventing session hijacking.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.wso2.carbon.apimgt:org.wso2.carbon.apimgt.apiMaven | < 9.31.117 | 9.31.117 |
org.wso2.carbon.apimgt:org.wso2.carbon.apimgt.rest.api.publisher.v1Maven | < 9.31.117 | 9.31.117 |
Affected products
3- Range: 4.5.0
- WSO2/WSO2 Carbon API Management APIv5Range: 6.7.206
- WSO2/WSO2 Universal Gatewayv5Range: 4.5.0
Patches
11b3496c072ecMerge pull request #13099 from Dakshithas/fix-3902-api-and-product-doc-upload-mediaType-validation
4 files changed · +83 −27
components/apimgt/org.wso2.carbon.apimgt.api/src/main/java/org/wso2/carbon/apimgt/api/ExceptionCodes.java+3 −1 modified@@ -807,7 +807,9 @@ public enum ExceptionCodes implements ErrorHandler { "defined as a primary endpoint", 400, "Failed to delete API endpoint with UUID '%s' since it is defined as a primary endpoint."), API_ENDPOINT_URL_INVALID(902049, "Endpoint URL is invalid", 400, - "Endpoint URL is invalid"); + "Endpoint URL is invalid"), + INVALID_MEDIA_TYPE_VALIDATION(902050, "Invalid or mismatched media type detected.", 415, + "File extension '%s' does not match detected MIME type '%s'"); private final long errorCode; private final String errorMessage; private final int httpStatusCode;
components/apimgt/org.wso2.carbon.apimgt.rest.api.publisher.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/publisher/v1/impl/ApiProductsApiServiceImpl.java+12 −6 modified@@ -194,9 +194,9 @@ public Response getAPIProductDocumentContent(String apiProductId, } @Override - public Response addAPIProductDocumentContent(String apiProductId, String documentId, - String ifMatch, InputStream fileInputStream, Attachment fileDetail, String inlineContent, - MessageContext messageContext) { + public Response addAPIProductDocumentContent(String apiProductId, String documentId, String ifMatch, + InputStream fileInputStream, Attachment fileDetail, String inlineContent, MessageContext messageContext) + throws APIManagementException { try { String organization = RestApiUtil.getValidatedOrganization(messageContext); APIProvider apiProvider = RestApiCommonUtil.getLoggedInUserProvider(); @@ -219,9 +219,13 @@ public Response addAPIProductDocumentContent(String apiProductId, String documen if (!documentation.getSourceType().equals(Documentation.DocumentSourceType.FILE)) { RestApiUtil.handleBadRequest("Source type of product document " + documentId + " is not FILE", log); } - RestApiPublisherUtils - .attachFileToProductDocument(apiProductId, documentation, fileInputStream, fileDetail, - organization); + String filename = fileDetail.getContentDisposition().getFilename(); + if (APIUtil.isSupportedFileType(filename)) { + RestApiPublisherUtils.attachFileToProductDocument(apiProductId, documentation, fileInputStream, + fileDetail, organization); + } else { + RestApiUtil.handleBadRequest("Unsupported extension type of document file: " + filename, log); + } } else if (inlineContent != null) { if (!documentation.getSourceType().equals(Documentation.DocumentSourceType.INLINE) && !documentation .getSourceType().equals(Documentation.DocumentSourceType.MARKDOWN)) { @@ -251,6 +255,8 @@ public Response addAPIProductDocumentContent(String apiProductId, String documen RestApiUtil.handleAuthorizationFailure( "Authorization failure while adding content to the document: " + documentId + " of API Product " + apiProductId, e, log); + } else if (e.getErrorHandler() != ExceptionCodes.INTERNAL_ERROR) { + throw e; } else { RestApiUtil.handleInternalServerError("Failed to add content to the document " + documentId, e, log); }
components/apimgt/org.wso2.carbon.apimgt.rest.api.publisher.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/publisher/v1/impl/ApisApiServiceImpl.java+4 −2 modified@@ -1727,8 +1727,8 @@ public Response getAPIDocumentContentByDocumentId(String apiId, String documentI * @return updated document as DTO */ @Override - public Response addAPIDocumentContent(String apiId, String documentId, String ifMatch, - InputStream inputStream, Attachment fileDetail, String inlineContent, MessageContext messageContext) { + public Response addAPIDocumentContent(String apiId, String documentId, String ifMatch, InputStream inputStream, + Attachment fileDetail, String inlineContent, MessageContext messageContext) throws APIManagementException { try { String organization = RestApiUtil.getValidatedOrganization(messageContext); APIProvider apiProvider = RestApiCommonUtil.getLoggedInUserProvider(); @@ -1787,6 +1787,8 @@ public Response addAPIDocumentContent(String apiId, String documentId, String if RestApiUtil.handleAuthorizationFailure( "Authorization failure while adding content to the document: " + documentId + " of API " + apiId, e, log); + } else if (e.getErrorHandler() != ExceptionCodes.INTERNAL_ERROR) { + throw e; } else { RestApiUtil.handleInternalServerError("Failed to add content to the document " + documentId, e, log); }
components/apimgt/org.wso2.carbon.apimgt.rest.api.publisher.v1/src/main/java/org/wso2/carbon/apimgt/rest/api/publisher/v1/utils/RestApiPublisherUtils.java+64 −18 modified@@ -32,8 +32,10 @@ import org.apache.tika.config.TikaConfig; import org.apache.tika.io.TikaInputStream; import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MimeTypes; import org.wso2.carbon.apimgt.api.APIManagementException; import org.wso2.carbon.apimgt.api.APIProvider; +import org.wso2.carbon.apimgt.api.ExceptionCodes; import org.wso2.carbon.apimgt.api.model.Documentation; import org.wso2.carbon.apimgt.api.model.OperationPolicyData; import org.wso2.carbon.apimgt.impl.APIConstants; @@ -92,7 +94,6 @@ public static void attachFileToDocument(String apiId, Documentation documentatio RestApiUtil.handleInternalServerError("Failed to add content to the document " + documentId, log); } - InputStream docInputStream = null; try { ContentDisposition contentDisposition = fileDetails.getContentDisposition(); String filename = contentDisposition.getParameter(RestApiConstants.CONTENT_DISPOSITION_FILENAME); @@ -102,23 +103,25 @@ public static void attachFileToDocument(String apiId, Documentation documentatio "Couldn't find the name of the uploaded file for the document " + documentId + ". Using name '" + filename + "'"); } + //APIIdentifier apiIdentifier = APIMappingUtil // .getAPIIdentifierFromUUID(apiId, tenantDomain); Path resolvedPath = resolveFilePath(docFile.getAbsolutePath(), filename); RestApiUtil.transferFile(inputStream, resolvedPath.getFileName().toString(), resolvedPath.getParent().toString()); - docInputStream = new FileInputStream(resolvedPath.toString()); - String mediaType = fileDetails.getHeader(RestApiConstants.HEADER_CONTENT_TYPE); - mediaType = mediaType == null ? RestApiConstants.APPLICATION_OCTET_STREAM : mediaType; - PublisherCommonUtils - .addDocumentationContentForFile(docInputStream, mediaType, filename, apiProvider, apiId, - documentId, organization); - docFile.delete(); + byte[] fileBytes = FileUtils.readFileToByteArray(new File(resolvedPath.toString())); + String mediaType = detectAndValidateMediaType(fileBytes, filename); + try (InputStream uploadStream = new ByteArrayInputStream(fileBytes)) { + PublisherCommonUtils.addDocumentationContentForFile(uploadStream, mediaType, filename, apiProvider, + apiId, documentId, organization); + } } catch (FileNotFoundException e) { RestApiUtil.handleInternalServerError("Unable to read the file from path ", e, log); + } catch (IOException e) { + RestApiUtil.handleInternalServerError("Error processing file upload for document: " + documentId, e, log); } finally { - IOUtils.closeQuietly(docInputStream); + FileUtils.deleteQuietly(docFile); } } @@ -181,7 +184,6 @@ public static void attachFileToProductDocument(String productId, Documentation d RestApiUtil.handleInternalServerError("Failed to add content to the document " + documentId, log); } - InputStream docInputStream = null; try { ContentDisposition contentDisposition = fileDetails.getContentDisposition(); String filename = contentDisposition.getParameter(RestApiConstants.CONTENT_DISPOSITION_FILENAME); @@ -197,17 +199,18 @@ public static void attachFileToProductDocument(String productId, Documentation d Path resolvedPath = resolveFilePath(docFile.getAbsolutePath(), filename); RestApiUtil.transferFile(inputStream, resolvedPath.getFileName().toString(), resolvedPath.getParent().toString()); - docInputStream = new FileInputStream(resolvedPath.toString()); - String mediaType = fileDetails.getHeader(RestApiConstants.HEADER_CONTENT_TYPE); - mediaType = mediaType == null ? RestApiConstants.APPLICATION_OCTET_STREAM : mediaType; - PublisherCommonUtils - .addDocumentationContentForFile(docInputStream, mediaType, filename, apiProvider, productId, - documentId, organization); - docFile.delete(); + byte[] fileBytes = FileUtils.readFileToByteArray(new File(resolvedPath.toString())); + String mediaType = detectAndValidateMediaType(fileBytes, filename); + try (InputStream uploadStream = new ByteArrayInputStream(fileBytes)) { + PublisherCommonUtils.addDocumentationContentForFile(uploadStream, mediaType, filename, apiProvider, + productId, documentId, organization); + } } catch (FileNotFoundException e) { RestApiUtil.handleInternalServerError("Unable to read the file from path ", e, log); + } catch (IOException e) { + RestApiUtil.handleInternalServerError("Error processing file upload for document: " + documentId, e, log); } finally { - IOUtils.closeQuietly(docInputStream); + FileUtils.deleteQuietly(docFile); } } @@ -353,6 +356,49 @@ public static String detectMediaType(InputStream inputStream) { return null; } + /** + * Detects the MIME type of a file based on its byte content and validates whether the file extension matches the + * detected MIME type. + * + * @param fileBytes the byte content of the file to validate + * @param filename the name of the file, used to extract the extension for validation + * @return the detected MIME type as a string if the extension matches the MIME type + * @throws APIManagementException if the fileBytes or filename is null, or if the MIME type detection or validation fails + */ + public static String detectAndValidateMediaType(byte[] fileBytes, String filename) throws APIManagementException { + if (fileBytes == null || filename == null) { + throw new APIManagementException(ExceptionCodes.INVALID_MEDIA_TYPE_VALIDATION); + } + + String detectedMimeType; + try (InputStream mimeDetectStream = new ByteArrayInputStream(fileBytes)) { + detectedMimeType = TikaConfig.getDefaultConfig().getDetector() + .detect(TikaInputStream.get(mimeDetectStream), new Metadata()).toString(); + } catch (Exception e) { + throw new APIManagementException("Error detecting media type", e, + ExceptionCodes.INVALID_MEDIA_TYPE_VALIDATION); + } + + int lastDot = filename.lastIndexOf('.'); + String fileExtension = (lastDot == -1) ? "" : filename.substring(lastDot + 1).toLowerCase(); + + String expectedExtension = ""; + try { + expectedExtension = MimeTypes.getDefaultMimeTypes().forName(detectedMimeType).getExtension() + .replace(".", ""); + } catch (Exception e) { + throw new APIManagementException("Error resolving expected extension", e, + ExceptionCodes.INVALID_MEDIA_TYPE_VALIDATION); + } + + if (!expectedExtension.equalsIgnoreCase(fileExtension)) { + throw new APIManagementException( + ExceptionCodes.from(ExceptionCodes.INVALID_MEDIA_TYPE_VALIDATION, fileExtension, detectedMimeType)); + } + + return detectedMimeType; + } + /** * This method will validate the given input stream for the allowed Media Types *
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-cmjc-qp7j-xgwrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-4760ghsaADVISORY
- security.docs.wso2.com/en/latest/security-announcements/security-advisories/2025/WSO2-2025-4104/mitrevendor-advisory
- github.com/wso2/carbon-apimgt/commit/1b3496c072ec68aaaf726996e2caa76f07c1adcaghsaWEB
- github.com/wso2/carbon-apimgt/pull/13099ghsaWEB
- mvnrepository.com/artifact/org.wso2.carbon.apimgt/org.wso2.carbon.apimgt.api/9.31.117ghsaWEB
- security.docs.wso2.com/en/latest/security-announcements/security-advisories/2025/WSO2-2025-4104ghsaWEB
News mentions
0No linked articles in our index yet.