Skip to content

Commit d2310a5

Browse files
Copilotcfc4n
andauthored
Fix JS code injection in browser_hover via selector escaping (CWE-94) (#45)
* Initial plan * Fix JS code injection in handleHover: use json.Marshal to escape selector Co-authored-by: cfc4n <709947+cfc4n@users.noreply.github.com> * Fix gofmt formatting in browser_test.go Co-authored-by: cfc4n <709947+cfc4n@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cfc4n <709947+cfc4n@users.noreply.github.com>
1 parent 0e532ce commit d2310a5

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

pkg/services/browser/browser.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,12 @@ func (bs *BrowserServer) handleHover(ctx context.Context, request mcp.CallToolRe
458458
var res bool
459459
runCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)
460460
defer cancelFunc()
461-
err := chromedp.Run(runCtx, chromedp.Evaluate(`document.querySelector('`+selector+`').dispatchEvent(new Event('mouseover'))`, &res))
461+
// Use json.Marshal to safely embed the selector in JS, preventing code injection.
462+
selectorJSON, err := json.Marshal(selector)
463+
if err != nil {
464+
return mcp.NewToolResultError(fmt.Sprintf("invalid selector: %s", err.Error())), nil
465+
}
466+
err = chromedp.Run(runCtx, chromedp.Evaluate(`document.querySelector(`+string(selectorJSON)+`).dispatchEvent(new Event('mouseover'))`, &res))
462467
if err != nil {
463468
return mcp.NewToolResultError(fmt.Errorf("failed to hover over element: %s", err.Error()).Error()), nil
464469
}

pkg/services/browser/browser_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package browser
1818

1919
import (
20+
"encoding/json"
21+
"strings"
2022
"testing"
2123

2224
"github.com/gojue/moling/pkg/comm"
@@ -43,3 +45,59 @@ func TestBrowserServer(t *testing.T) {
4345
t.Fatalf("Failed to create BrowserServer: %s", err.Error())
4446
}
4547
}
48+
49+
// TestHoverSelectorEscaping verifies that the selector is safely JSON-encoded
50+
// before being embedded in the JavaScript expression, preventing code injection.
51+
func TestHoverSelectorEscaping(t *testing.T) {
52+
tests := []struct {
53+
name string
54+
selector string
55+
// wantPrefix checks that the selector is embedded as a JSON-encoded double-quoted string
56+
wantJSPrefix string
57+
wantJSSuffix string
58+
}{
59+
{
60+
name: "normal selector",
61+
selector: "body",
62+
wantJSPrefix: `document.querySelector("body").dispatchEvent`,
63+
},
64+
{
65+
name: "injection attempt with single quotes and comma operator",
66+
selector: "body'),document.title='PWNED',document.querySelector('body",
67+
// After JSON encoding, the selector is a double-quoted string literal.
68+
// The single quotes and commas stay inside the string and are NOT executable.
69+
wantJSPrefix: `document.querySelector("body'),document.title='PWNED',document.querySelector('body").dispatchEvent`,
70+
},
71+
{
72+
name: "injection attempt with semicolons and IIFE",
73+
selector: "body'); (function(){ /* exfiltration */ })(); document.querySelector('body",
74+
wantJSPrefix: `document.querySelector("body'); (function(){ /* exfiltration */ })(); document.querySelector('body").dispatchEvent`,
75+
},
76+
{
77+
name: "selector with double quotes is escaped by JSON",
78+
selector: `div[data-id="foo"]`,
79+
// json.Marshal escapes inner double quotes as \", so they cannot break out of the JS string.
80+
wantJSPrefix: `document.querySelector("div[data-id=\"foo\"]").dispatchEvent`,
81+
},
82+
}
83+
84+
for _, tc := range tests {
85+
t.Run(tc.name, func(t *testing.T) {
86+
selectorJSON, err := json.Marshal(tc.selector)
87+
if err != nil {
88+
t.Fatalf("json.Marshal failed: %v", err)
89+
}
90+
js := `document.querySelector(` + string(selectorJSON) + `).dispatchEvent(new Event('mouseover'))`
91+
92+
// The embedded selector must be wrapped in JSON double-quotes (not single-quotes).
93+
// This ensures injected single-quote characters cannot break out of the JS string context.
94+
if !strings.HasPrefix(string(selectorJSON), `"`) || !strings.HasSuffix(string(selectorJSON), `"`) {
95+
t.Errorf("selector was not JSON-encoded as a double-quoted string: %s", string(selectorJSON))
96+
}
97+
98+
if tc.wantJSPrefix != "" && !strings.HasPrefix(js, tc.wantJSPrefix) {
99+
t.Errorf("JS expression did not start with expected prefix\n want: %s\n got: %s", tc.wantJSPrefix, js)
100+
}
101+
})
102+
}
103+
}

0 commit comments

Comments
 (0)