Skip to content

Stored XSS via XssHtmlFilter Bypass #38

@Arron-bit

Description

@Arron-bit

Summary

A Stored Cross-Site Scripting (XSS) vulnerability exists in SpringBlade's /api/blade-desk/notice/submit endpoint. By leveraging JSON Unicode escape sequences (\uXXXX), an attacker can completely bypass the XssHtmlFilter and inject arbitrary JavaScript into the content field. Since the notice system allows low-privilege users to send messages to high-privilege users (e.g., administrators), this can lead to session hijacking, privilege escalation, and arbitrary API execution.

Affected Versions

Steps to Reproduce

1. Inject Malicious Payload

Send the following request to the notice submit endpoint. The content field contains an XSS payload encoded with JSON Unicode escapes to bypass XssHtmlFilter \u003cimg src=x onerror=alert(1)\u003e:

Image Image

2. Trigger XSS

When the victim (e.g., an administrator) views the notice:

Image

The response contains the unescaped HTML, and when rendered in the browser, the JavaScript alert(1) executes automatically without any user interaction (no click required).

Root Cause Analysis

The vulnerability stems from an architectural flaw in how XssHttpServletRequestWrapper applies HTML-based filtering to JSON request bodies.

Processing Flow

Client sends JSON body with \u003c (JSON Unicode for '<')
        │
        ▼
┌─────────────────────────────────┐
│  XssHttpServletRequestWrapper   │
│  getInputStream()               │
│  ┌───────────────────────────┐  │
│  │ Reads raw body as String  │  │
│  │ Passes to XssHtmlFilter   │──┼──► XssHtmlFilter.filter() sees \u003c
│  │                           │  │    as literal characters '\', 'u', '0'...
│  │                           │  │    No '<' or '>' found.
│  │                           │  │    All regex patterns fail to match.
│  │                           │  │    ──► INPUT PASSES THROUGH UNMODIFIED
│  └───────────────────────────┘  │
└─────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────┐
│  Jackson JSON Deserializer      │
│  Decodes \u003c → '<'           │
│  Decodes \u003e → '>'           │
│  Result: <img src=x onerror=..> │
└─────────────────────────────────┘
        │
        ▼
   Stored in Database as raw HTML
        │
        ▼
   Rendered in victim's browser → XSS triggered

Key Code - XssHttpServletRequestWrapper.java

@Override
public ServletInputStream getInputStream() throws IOException {
    // ...
    // Reads the raw JSON body
    String body = WebUtil.getRequestBody(super.getInputStream());
    // Applies HTML-based filtering to the entire JSON string
    body = xssEncode(body);
    // ...
}

private String xssEncode(String input) {
    return HTML_FILTER.filter(input);  // XssHtmlFilter
}

Key Code - XssHtmlFilter.java

The filter relies entirely on regex patterns to detect HTML tags:

private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", 32);
private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", 34);
private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", 34);

These patterns only match literal < and > characters. JSON Unicode escapes (\u003c, \u003e) are not < / > at the byte level, so every regex check passes without detection. The filter is fundamentally unaware of JSON encoding.

Why This Works

Stage Sees Action
XssHtmlFilter \u003cscript\u003e (literal backslash + u + hex digits) No < or > found → passes through
Jackson \u003c Decodes to < per JSON spec (RFC 8259)
Database <script>alert(1)</script> Stored as raw HTML
Browser <script>alert(1)</script> Executes JavaScript

Remediation

  1. Do not filter raw JSON bodies as HTML. The XssHttpServletRequestWrapper should not apply XssHtmlFilter to the raw request body. Instead, sanitize individual field values after JSON deserialization (e.g., via a Jackson deserializer or a Spring @ControllerAdvice).
  2. Use a proven HTML sanitizer. Replace the custom regex-based XssHtmlFilter with a well-tested library such as OWASP Java HTML Sanitizer or jsoup Safelist.
  3. Apply output encoding. Ensure all user-supplied content is HTML-encoded at render time on the frontend, regardless of backend filtering.

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