Date: 2026-05-19 Project: WebOperator Scope: TypeScript React Chrome extension, Node.js native messaging bridge, local HTTP/socket API.
The project already has several good controls: the local bridge binds to 127.0.0.1 by default, query-string API tokens are rejected, Chrome Native Messaging restricts allowed extension origins, credentials are stored in chrome.storage.session, and the new verification gate passes npm audit, ESLint, tests, Knip, ShellCheck, and build.
All findings in this report are fixed in the current working tree and covered by regression/smoke checks where applicable.
Status update: SBP-1 has been fixed in the current working tree. The original finding is kept below for audit trail, followed by the fixed evidence.
- Rule:
REACT-XSS-001,REACT-XSS-002,JS-XSS-001 - Severity: High
- Status: Fixed in
core/src/sidepanel/App.tsx; regression tests added incore/src/sidepanel/App.test.ts - Location:
core/src/sidepanel/App.tsx,AnswerPanel, lines 517 and 1133-1154 - Evidence:
Original vulnerable pattern:
<div className="answer-text" dangerouslySetInnerHTML={{ __html: renderMarkdown(answer ?? fallback) }} />html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');Fixed pattern:
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label: string, rawHref: string) => renderSafeLink(label, rawHref));function normalizeSafeLinkHref(rawHref: string): string | null {
const href = rawHref.trim();
if (!href) return null;
try {
const url = new URL(href);
if (url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'mailto:') {
return url.href;
}
} catch {
return null;
}
return null;
}- Impact: A crafted final answer such as markdown containing quotes in the URL target can break out of the
hrefattribute. Ajavascript:URL can also be rendered as a clickable link. Since final answers may summarize untrusted page content through an LLM, a malicious webpage can try to influence the answer and turn the side panel into a DOM XSS sink. - Fix: Replace the regex-based HTML renderer with a safe renderer, or at minimum escape attribute values and only allow
http:,https:, andmailto:link protocols. For untrusted markdown, prefer a vetted sanitizer such as DOMPurify before usingdangerouslySetInnerHTML. - Mitigation: Add a regression test for malicious markdown links, including quote injection and
javascript:URLs. - Verification:
core/src/sidepanel/App.test.tscovers attribute breakout,javascript:URLs, and safe HTTPS links. - False positive notes: The initial
&,<, and>escaping reduces raw tag injection, but it does not escape quotes used inside generated attributes and does not validate URL protocols.
- Rule:
EXPRESS-INPUT-001/ general auth hardening for local privileged APIs - Severity: Medium
- Status: Fixed in
weboperator-bridge/bridge.js; unauthenticated mode now requires explicitWEBOPERATOR_ALLOW_UNAUTHENTICATED_BRIDGE=1 - Location:
weboperator-bridge/bridge.js,enforceAuthandenforceAgentAuth, lines 190-198 and 398-401 - Evidence:
function enforceAuth(req, url) {
if (!API_TOKEN) return;function enforceAgentAuth(msg) {
if (!API_TOKEN) return;- Impact: When
WEBOPERATOR_API_TOKENis unset, any local process that can reach127.0.0.1:8765or the Unix socket can attempt to drive the extension API. That API can request browser snapshots, screenshots, navigation, typing, and task actions. Binding to loopback is good, but this is still a privileged local control surface. - Fix: Generate a per-install random token by default, or require
WEBOPERATOR_API_TOKENunless an explicitWEBOPERATOR_ALLOW_UNAUTHENTICATED_BRIDGE=1development flag is set. - Mitigation: Keep the bridge bound to
127.0.0.1, keep the socket chmod0600, and document that unauthenticated mode is development-only. - False positive notes: This is less severe than a network-exposed API because the default bind host is loopback and browser CORS blocks many web-origin reads. It remains relevant for local process/browser-extension threat models.
- Rule: general secret/logging hygiene
- Severity: Low
- Status: Fixed in
weboperator-bridge/bridge.js; argv logging now redacts common token/secret/password/key flags - Location:
weboperator-bridge/bridge.js, lines 10 and 18-22 - Evidence:
Original pattern:
const LOG = process.env.WEBOPERATOR_BRIDGE_LOG || '/tmp/weboperator-bridge.log';
log(`process start pid=${process.pid} ppid=${process.ppid} argv=${JSON.stringify(process.argv)} cwd=${process.cwd()}`);Fixed pattern:
log(`process start pid=${process.pid} ppid=${process.ppid} argv=${JSON.stringify(redactArgv(process.argv))} cwd=${process.cwd()}`);- Impact: The current default log does not include environment variables, so
WEBOPERATOR_API_TOKENis not logged. However, if future launch modes pass sensitive values through CLI args,argvlogging would persist them to a predictable temp path. - Fix: Keep secrets out of CLI args and consider redacting known-sensitive argv flags before logging.
- Mitigation: Prefer env vars for secrets and keep
/tmp/weboperator-bridge.loglocal-only. - False positive notes: Current code and installer pass the bridge path via
args, not secrets.
docs/api.mdstates query-string tokens are not supported, andscripts/bridge-smoke.mjsverifies?token=returns401.weboperator-bridge/bridge.jsbinds the HTTP bridge to127.0.0.1by default.weboperator-bridge/install.shwrites a Chrome Native Messaging manifest with a specificallowed_originsextension ID.core/src/lib/storage.tsstores saved credentials inchrome.storage.session, not persistent local storage.npm audit --audit-level=moderatecurrently reports0 vulnerabilities.