VYPR
Moderate severityNVD Advisory· Published Sep 23, 2025· Updated Sep 23, 2025

Authenticated Stored Cross-Site Scripting (XSS) in Multiple WSO2 Products via API Document Upload in Publisher

CVE-2025-4760

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.

PackageAffected versionsPatched versions
org.wso2.carbon.apimgt:org.wso2.carbon.apimgt.apiMaven
< 9.31.1179.31.117
org.wso2.carbon.apimgt:org.wso2.carbon.apimgt.rest.api.publisher.v1Maven
< 9.31.1179.31.117

Affected products

3
  • WSO2/WSO2 Carbon API Management APIv5
    Range: 6.7.206
  • WSO2/WSO2 Universal Gatewayv5
    Range: 4.5.0

Patches

1
1b3496c072ec

Merge pull request #13099 from Dakshithas/fix-3902-api-and-product-doc-upload-mediaType-validation

https://github.com/wso2/carbon-apimgtAshera SilvaMay 15, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.