Skip to content

fix: close an auth bypass, stop tools reporting failure as success, remove unreachable code#59

Merged
aaronjmars merged 7 commits into
mainfrom
optimize/cleanup
Jul 21, 2026
Merged

fix: close an auth bypass, stop tools reporting failure as success, remove unreachable code#59
aaronjmars merged 7 commits into
mainfrom
optimize/cleanup

Conversation

@aaronjmars

Copy link
Copy Markdown
Collaborator

Output of a repo-wide cleanup pass. It turned up more defects than cruft, so
most of this is fixes; the deletions are the parts that provably could not run.

Net: −1527 / +260 across 7 commits, kept separate so any one can be reverted alone.

Security — please read first

00188f2 --token= disabled authentication entirely. The flag is matched with
startsWith('--token='), so a bare --token= yielded an empty token, and the request
guard treats an empty token as "no auth configured" and passes every request through.
The startup banner is gated the same way, so nothing is printed — --tunnel --token=
published an unauthenticated browser-control endpoint with no visible sign. This
defeated the boundary added in #55. --http-host= had the same falsy-vs-nullish bug
and bound all interfaces; together they opened remote browser control to the network.

Verified: 401 / 401 / 200 for missing / wrong / correct bearer token, and each
malformed flag now exits with a named error instead of an unhandled rejection.

Tools no longer report failure as success

8b4cdba An MCP client acts on what these tools say, so a failure dressed as an empty
result is worse than an error.

Tool Before
get_history API/permission failure rendered as "No history items found", isError: false
get_selected_text four distinct failures rendered as "No text selected"
page_navigate claimed success when its wait condition timed out (the warning field was set and never read)
research workflow claimed "Current page bookmarked" after the bookmark call failed

Same commit: a dropped connection no longer executes tools anyway. createConnection
resolved without waiting for the socket to open and swallowed failures, so tab_close
and element_click ran against the browser while the reply went to a still-connecting
socket and was discarded — the model saw a generic "Tool call timeout" and would
reasonably retry a side effect.

Removed — unreachable, not merely unused

9db31ea The enhanced-pattern tier never executed: its guard read bestMethod, an
identifier declared nowhere in the repo, so it threw on every call and the catch below
swallowed it, silently degrading page_analyze to a viewport scan. The legacy
single-phase engine went with it — reaching it required a phase value both tool
schemas forbid. The prompts/* workflows were unreachable per spec: no prompts
capability was ever declared, and prompts/get returned {content:[...]} where the
spec requires {messages:[...]}.

page_analyze output is unchanged, because none of the removed code could run.

Fixes and quick wins

6fe5294 CI's "Validate builds" step could not fail (validateAllBuilds()'s promise was
discarded) — now exits 1, verified by deliberately breaking a manifest · page_navigate
crashed when no window has focus · a malformed register frame from any loopback client
broke tools/list until restart · NaN shipped to the model from divide-by-zero · two
dead protocol halves · a dead IntersectionObserver per page load · popup said 17 tools
and omitted page_style.

6c859a6 page_scroll silently dropped its position data (formatter read field names the
content script never emits) · tab_list always said "Active tab: None" · page_style
effects never expired (schema defaults weren't applied) · the reconnect give-up guard
was unreachable · three disagreeing version strings unified.

Verification

node --check, build, build.js validate, structure tests and web-ext lint are green:
0 errors, the same 3 pre-existing warnings, 19 structure markers. Behavior was checked by
running it — auth enforcement, flag validation, a live WS + /sse round trip, the
malformed-register guard, and version consistency.

One thing that is NOT verified

The connection-lifecycle changes in 8b4cdba — awaiting socket open, send() throwing,
the Chrome socket-reuse guard — need a manual load in Chrome and Firefox. This repo has
no behavioral test coverage, so a green gate proves "still parses", not "still works".
That commit is deliberately separable.

Related and still open: Safety Mode may not work on Firefox at all (background.js
uses the Chrome callback form of storage.local.get, and the vendored polyfill throws
on over-arity) — worth confirming in the same manual pass.

Deletions of code that never executes or is never read. No user- or
model-visible string changes, so tool output is byte-identical.

- server.js: unused `createServer` import; `killExisting` (parsed, never
  read — the kill at startup is unconditional); `organizedTabs`;
  `currentTime`/`oneHourAgo` (computed, never used — the archive_old rule
  is the tab.index heuristic below them)
- server.js: the `settings_used` block in formatTabCreateResult. The
  extension's createTabsBatch never sets that field, so the branch could
  not be entered.
- background.js: `generateTestUrls`, `estimateBatchTime` — no call sites
- content.js: `estimateTokenUsage` — no call sites; unused `startTime`
- popup.js: `toolsList` — built and never referenced
- server.js: strip `ADD:`/`START:` prefixes and a stale port-migration
  note from comments describing changes rather than code

Gate: node --check, build, validate, structure tests, web-ext lint all
green; web-ext warning count unchanged at 3.
…ing open

`args.find(a => a.startsWith('--token='))` matches the bare flag, so
`--token=` produced AUTH_TOKEN = "" and requireToken's `if (!AUTH_TOKEN)
return next()` waved every request through. The startup banner is gated on
`if (AUTH_TOKEN)` too, so nothing was printed: `--tunnel --token=` published
an unauthenticated browser-control endpoint with no visible sign. This
defeated the loopback/token boundary added in ea71bb9.

`--http-host=` had the same falsy-vs-nullish bug, binding 0.0.0.0 via
`app.listen(PORT, "")`. Together (`--http-host= --token=`) they opened
remote browser control to the network.

Parse flags through argValue(), which returns null when a flag is absent
and errors when its value is empty, so an empty value can no longer be
mistaken for "not set". Ports go through portValue(): digits only, since
Number() accepts '0x1f' and parseInt() accepts '80abc'. A bad port used to
reach net.listen(NaN), which throws synchronously and — with no .catch on
startServer() — died as an unhandled rejection; startServer() now reports
the real message and exits 1.

Verified: --token= / --http-host= / --port=abc / --port=0x10 each exit with
a named error; default startup unchanged; --http-host=0.0.0.0
--token=mysecret123 binds 0.0.0.0 and returns 401 / 401 / 200 for
missing / wrong / correct bearer tokens.
An MCP client acts on what these tools report, so a failure dressed as an
empty result is worse than an error.

get_history: the catch returned history_items: [], and formatHistoryResult
branches only on that array being empty -- it never reads success/error. A
permissions failure rendered as "No history items found" with isError:false.
Now rethrows, so it reaches the JSON-RPC error boundary with its message.

get_selected_text: four failure paths (tab missing, no active tab, script
returned nothing, outer catch) returned objects lacking has_selection, which
formatSelectedTextResult renders as "No text selected". Now throw. The
genuine no-selection case still returns success with has_selection: false.

page_navigate: a failed wait condition returned success:true plus a warning
field the formatter never read, so the model was told navigation succeeded.
The warning is now surfaced (tab_create already did this).

Research workflow: appended "Current page bookmarked for reference"
unconditionally after swallowing the bookmark failure into console.warn.
Now reports what actually happened.

Connection handling: createConnection resolved without waiting for the
socket to open and swallowed failures, so handleMCPRequest proceeded into
the tool switch anyway -- tab_close and element_click executed for real
while the reply was written to a CONNECTING socket and dropped. The model
saw only "Tool call timeout" and would reasonably retry a side effect.
createConnection now awaits open and rethrows; the Chrome branch reuses a
live socket instead of replacing the one the request arrived on; send()
throws rather than dropping a frame; fire-and-forget reconnect paths catch
explicitly; the popup's reconnect button reports the real outcome.

Verified: full gate green (0 errors, same 3 pre-existing web-ext warnings,
19 structure markers unchanged); end-to-end round trip over WS + /sse
confirms page_navigate renders the warning instead of success.

NOT verified: the connection-lifecycle changes need a manual load in Chrome
and Firefox -- no automated coverage exists for them.
Two subsystems that no caller could reach.

content.js -- the enhanced-pattern tier never ran. Its guard read
`bestMethod`, an identifier declared nowhere in the repo, so evaluating it
threw ReferenceError on every page where the anti-detection branch had not
already filled quickMatches. The catch below logged "Enhanced patterns
failed" and fell through to quickViewportScan, so the failure was invisible
and tryEnhancedPatterns never executed once.

The legacy single-phase engine was unreachable for a different reason:
analyzePage only reaches it when `phase` is neither "discover" nor
"detailed", but phase defaults to "discover" and is declared
enum: ["discover", "detailed"] in both the extension's registry and the
server's fallback copy -- so reaching it required a schema-forbidden value.

Removed as one closed set, since legacyAnalysis was the only other caller of
tryEnhancedPatterns: legacyAnalysis, tryEnhancedPatterns, ENHANCED_PATTERNS,
parseIntent, findPatternElements, tryUniversalPatterns, tryPatternDatabase,
detectSite, getUniversalPattern, PATTERN_DATABASE, trySemanticAnalysis,
formatAnalysisResult, and isVisible (superseded everywhere by
isLikelyVisible). The dispatch is now total on the declared enum.

server.js -- the prompts system was unreachable per the MCP spec: the
initialize response declares capabilities: { tools: {} } with no prompts
capability, and prompts/get returned { content: [...] } where the spec
requires { messages: [{ role, content }] }. Removed prompts/list,
prompts/get and the six execute*Workflow functions.

page_analyze output is unchanged: no removed code could execute.

Verified: no dangling references to any removed symbol; gate green (0
errors, same 3 pre-existing web-ext warnings, 19 structure markers); live
round trip over WS + /sse still registers tools, answers tools/list, and
renders page_navigate correctly.
build.js: `case 'validate'` discarded validateAllBuilds()'s promise, so the
CI "Validate builds" step passed regardless of the result. Now exits 1 on
failure — verified by breaking dist/chrome/manifest.json and confirming a
non-zero exit.

navigateToUrl: destructured tabs.query() and used activeTab.id with no
guard, so page_navigate crashed when no normal window has focus (devtools
undocked, all windows minimized). Every other caller of the same query
already guards; this one now does too.

register frame: `availableTools = message.tools` trusted the peer, and any
loopback client could send a register with no tools — leaving availableTools
undefined and making every later tools/list throw until restart. Now
validated as an array; verified tools/list survives a malformed frame.

Divide-by-zero: summarizeResults, summarizePosts and calculateQualityScore
divided by results.length/posts.length, which are 0 when an extractor
matches nothing — shipping avg_score: NaN and "NaN%" to the model.

Dead protocol halves: the background's `action: "test"` handler had no
sender and the message it emitted had no server-side branch; the popup's
`statusUpdate` listener had no sender (status comes from polling).

setupViewportAnalyzer: built an IntersectionObserver that never observed
anything and a visibilityMap that was never read — one dead allocation per
page load. isLikelyVisible does this work.

popup: said 17 tools and omitted page_style while the extension registers
18. Name list and count now derive from one array so they can't disagree;
verified the list covers all 18 registered tools.

logo.webm: referenced by build.js and popup.html but absent from the repo,
so the copy was permanently skipped and the <source> never matched.

EXTENSION_ORIGIN: the same regex was inlined in the WebSocket handshake
check; now uses the existing constant.

Gate green: 0 errors, same 3 pre-existing web-ext warnings, 19 structure
markers; live round trip unchanged.
…ect ceiling

formatScrollResult read scroll_position, wait_time, element_scrolled and
page_dimensions — none of which scrollPage returns. It emits new_position
and wait_after, so the position and wait were silently dropped from every
scroll result while four branches sat permanently false. Remapped the two
that have real counterparts; removed the two that don't.

formatTabListResult read result.active_tab, but listTabs returns it under
summary. "Active tab" therefore always rendered "None". Now reads
result.summary?.active_tab.

handlePageStyle destructured intensity and duration without defaults, while
the tool schema declares 'medium' and 10. applyEffect gates on
`duration > 0`, so effects were never auto-removed and the description
rendered "effect for undefineds". Defaults now match the schema.

Reconnect: createConnection reset reconnectAttempts to 0 after port
discovery, which runs whenever attempts > 2 — so the counter oscillated
0->3->0, backoff never grew past ~40s, and the "Maximum reconnection
attempts reached" guard was unreachable. The genuine reset in onopen stays.

Version: initialize advertised 2.0.0 and the SSE hello 1.0.0 while
package.json said 1.1.1. Single SERVER_VERSION constant; verified
initialize now reports 1.1.1, matching package.json.

Gate green: 0 errors, same 3 pre-existing web-ext warnings, 19 markers;
live round trip unchanged.
@aaronjmars
aaronjmars merged commit e27daf8 into main Jul 21, 2026
2 checks passed
@aaronjmars
aaronjmars deleted the optimize/cleanup branch July 21, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant