GHSA-69hx-63pv-f8f4 Ech0 has Stored XSS via SVG Upload and Content-Type Validation Bypass in File Upload
SummaryThe file upload endpoint validates Content-Type using only the client-supplied multipart header, with no server-side content inspection or file extension validation. Combined with an unauthenticated static file server that determines Content-Type from file extension, this allows an admin to upload HTML/SVG files containing JavaScript that execute in the application's origin when visited by any user. Additionally, `image/svg+xml` is in the default allowed types, enabling stored XSS via SVG without any Content-Type spoofing.DetailsThe upload handler at `internal/service/file/file.go:85-87` validates file type using only the multipart `Content-Type` header:`isAllowedType` at `file.go:836-843` performs exact string matching — no magic byte detection, no extension validation:The original file extension is preserved in the storage key by `RandomKeyGenerator` at `internal/storage/keygen.go:41`:All locally stored files are served publicly without authentication at `internal/router/modules.go:51`:This `gin.Static` call is registered directly on the engine, outside any authentication middleware group. Go's `http.ServeFile` (used internally by `gin.Static`) determines the response `Content-Type` using `mime.TypeByExtension`, so `.html` files are served as `text/html` and `.svg` files as `image/svg+xml`.No `X-Content-Type-Options: nosniff` or `Content-Security-Policy` headers are set (verified in `internal/router/middleware.go`).**Variant 1 — SVG XSS (no spoofing needed):** `image/svg+xml` is in the default `AllowedTypes` at `internal/config/config.go:241`. SVG files can contain `<script>` tags and event handlers. The VireFS schema routes `.svg` to `images/` (`internal/storage/schema.go:10`). Uploaded SVGs are publicly accessible at `/api/files/images/<key>.svg` and JavaScript within them executes in the application's origin.**Variant 2 — Content-Type spoofing:** Upload an `.html` file with a forged multipart `Content-Type: image/jpeg`. The allowlist check passes (image/jpeg is allowed). The `.html` extension is preserved. The VireFS schema routes unknown extensions to `files/` (`schema.go:14`). The file is served at `/api/files/files/<key>.html` as `text/html`.PoC**Variant 1 — SVG XSS (simplest, default config):****Variant 2 — Content-Type bypass with HTML:**Impact**Stored XSS in the application origin**: JavaScript executes in the context of the Ech0 application domain when any user visits the file URL directly.**Session hijacking**: Attacker script can access same-origin cookies and API endpoints, enabling theft of admin session tokens.**Persistent backdoor**: The malicious file remains on the unauthenticated static server even after the compromised admin account is secured or its credentials are rotated.**Data exfiltration**: JavaScript running in the application origin can call internal API endpoints (e.g., `/api/echo/page`) and exfiltrate application data.**Social engineering vector**: An admin (or attacker with admin credentials) plants the file; any user tricked into clicking the link is compromised.The admin-required upload limits initial access, but the persistent nature of the stored XSS and the unauthenticated static serving create a meaningful attack surface, particularly in multi-admin deployments or after admin account compromise.Recommended Fix**1. Validate Content-Type server-side using magic bytes** (`internal/service/file/file.go`):**2. Remove `image/svg+xml` from default AllowedTypes** or sanitize SVGs to strip `<script>` tags and event handlers before storage.**3. Add security headers** in `internal/router/middleware.go`:**4. Serve uploaded files with `Content-Disposition: attachment`** or from a separate origin/subdomain to isolate them from the application's cookie scope.
CVSS v3.1 score: 4.8