Skip to content

Multiple REST API Endpoints Exist Stored Cross-Site Scripting (XSS) Vulnerability via JSON Request Body XSS Filter Bypass #1093

@Arron-bit

Description

@Arron-bit

Affected Versions

version ≤ 3.2.5 (latest version at time of disclosure)

Repository: https://github.com/shopizer-ecommerce/shopizer

Impact

Multiple REST API endpoints, including PUT /api/v1/private/category/{id}, POST /api/v1/auth/products/{id}/reviews, PUT /api/v1/private/customer/{id}, POST /api/v1/private/content, and POST /api/v1/search, are vulnerable to Stored Cross-Site Scripting (XSS). An attacker can bypass the backend XSS filter by submitting malicious JavaScript payloads within the JSON request body. These payloads are persisted to the database and returned verbatim by the corresponding GET endpoints (e.g., GET /api/v1/category), executing arbitrary JavaScript in the victim's browser.

Shopizer implements a tiered role-based access control system with varying privilege levels (e.g., admin_content, admin_category). A low-privileged admin user can exploit this Stored XSS to attack a super administrator — for example, by injecting a payload that steals the super admin's session token or performs privileged operations on their behalf when they view the affected resource.

Steps to Reproduce

  1. Authenticate as a low-privileged admin user (e.g., one with admin_category role).
Image
  1. Send a PUT request to /api/v1/private/category/{id} with a malicious XSS payload injected into a category field description:
Image
  1. Send a GET request to the public endpoint /api/v1/category and observe that the response body contains the raw, unsanitized JavaScript payload.
Image
  1. When a super administrator (or any user) visits a frontend page that renders this category data, the injected script executes in their browser context, allowing the attacker to steal session cookies, perform CSRF, or escalate privileges.
Image

Root Cause Analysis

The vulnerability originates from an incomplete XSS filtering mechanism in the XssHttpServletRequestWrapper class.

File: sm-shop/src/main/java/com/salesmanager/shop/filter/XssHttpServletRequestWrapper.java

The wrapper only overrides getHeader(), getParameter(), and getParameterValues() to apply HTML sanitization via AntiSamy. Critically, it does not override getInputStream() or getReader():

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return SanitizeUtils.getSafeRequestParamString(value);
    }

    @Override
    public String getParameter(String parameter) {
        String value = super.getParameter(parameter);
        return SanitizeUtils.getSafeRequestParamString(value);
    }

    @Override
    public String[] getParameterValues(String parameter) {
        // ... sanitizes URL query parameters only
    }

    // ✗ getInputStream() is NOT overridden
    // ✗ getReader() is NOT overridden
}

In Spring MVC, all @RequestBody-annotated parameters are deserialized by reading from HttpServletRequest.getInputStream(). Since this method is not overridden, the JSON request body completely bypasses XSS filtering.

Nearly all REST API controllers in Shopizer use @RequestBody to accept JSON input. For example, the PersistableCategory model used by the category endpoint has no server-side HTML sanitization or validation on its name or description fields — only basic @NotEmpty constraints exist:

public class PersistableCategoryDescription extends CategoryDescription {
    @NotEmpty
    private String name;        // ✗ No XSS filtering or HTML escaping
    private String description;  // ✗ No XSS filtering or HTML escaping
}

This means any string field in any JSON-based API endpoint is a potential injection point.

Remediation

  1. Override getInputStream() and getReader() in XssHttpServletRequestWrapper to read, sanitize, and re-wrap the JSON request body before it reaches the Spring deserialization layer. Parse the body as a JSON tree, recursively sanitize all string values, then return a new ServletInputStream backed by the sanitized content.
  2. Enable security response headers in the Spring Security configuration: add Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and X-XSS-Protection: 1; mode=block.
  3. Register the XSS filter for all URL patterns, including /services/**.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions