Repository navigation
fix(browser): gate navigate on an allowed-scheme allowlist with the refusal before any logging, redact URL userinfo, and complete the reload cycle - #955
Conversation
|
Verification notes for review (head 1adee41 + the gate-order fix): Contract under test (from #911, our filing): the browser service accepted any non-blank URL on a plugin-exposed IPC surface — The change, in gate order:
Verification: 7/7 in Design note for the review: the parse requires an exact allowlist match on the pre- |
bdcf620 to
bc9aa3b
Compare
|
Thanks @choksi2212 for this PR. Before it can be reviewed or merged, it needs a rebase: against Could you rebase onto current |
|
Thanks for this, @choksi2212 - the change itself ("fix(browser): gate navigate on an allowed-scheme allowlist with the refusal befo") looks worthwhile and we'd love to land it. One thing blocking review: this branch currently carries 89 commits, most of them unrelated work from other branches, so the PR diff shows far more than its own change and can't be merged as-is. Could you rebase onto the current |
…L userinfo in logs, and complete the reload cycle Fixes risa-labs-inc#911. The browser service accepted any non-blank URL on a plugin-exposed IPC surface: a javascript: URL was executed in the page context with no approval gate, and the raw URL - including user:password@host credentials - was logged verbatim at INFO. A refused scheme now short-circuits before any state is touched; allowed schemes are http/https/file/ftp/about. URL logging redacts userinfo per the risa-labs-inc#640 shape. reload() also stranded the window in loading=true forever (STARTED emitted, COMPLETED never): the bookkeeping-only reload now completes the cycle and clears the flag. 4 regression tests, none collecting unbounded flows. (cherry picked from commit 1adee41)
…d pin smuggling/redaction contracts Re-audit follow-up on risa-labs-inc#911: the scheme gate now runs BEFORE the navigate INFO log - a refused javascript:/data: payload must never reach any log line (only its scheme does, at WARN) - closing the exact pre-gate leak rival PRs pitch against us. getFavicon's DEBUG URL is routed through the same userinfo redaction. Three new tests pin the contracts the re-audit found unpinned: uppercase/tab-injected/schemeless smuggling all refuse; a refused navigation leaves the current window's page state intact; and the redaction masks a password containing '@' end-to-end (https://admin:p@ss@w0rd@host -> https://***@host). (cherry picked from commit 792c2d0)
792c2d0 to
10a6f4b
Compare
|
Rebased onto current upstream
New tip:
Force-pushed to |
|
@claude please review this PR against the current head - gate review for merge readiness, focused on the security claims in the diff. |
|
Claude finished @fluck-boss's task in 3m 21s —— View job Gate review - merge readiness (security focus)
Verdict: do not merge as-is. This is a net downgrade of a gate that already exists.The headline isn't the rebase - it's the content. The branch's own base commit (
val NAVIGABLE_SCHEMES = setOf("http", "https")
logger.info("navigate: windowId={}, url={}", request.windowId, LogSanitizer.describeUri(url))
logger.debug("getFavicon: url={}", LogSanitizer.describeUri(request.url))So "gate before log" and "redact Blocking: the diff weakens the existing gate1. private val allowedSchemes = setOf("http", "https", "file", "ftp", "about")Base/dev deliberately refuse both, and say why in the KDoc that this diff deletes: 2. The authority gate is removed entirely. 3. 4. Blocking: bugs in the new implementation5. The refusal path echoes an unbounded attacker-controlled substring into the log and the response — val scheme = url.substringBefore("://", "").lowercase()
...
logger.warn("navigate: refused scheme={}, windowId={}", scheme.ifBlank { "none" }, windowId)
"Refused navigation: unsupported URL scheme '$scheme'"
This directly contradicts the two central claims in the description — "the raw URL is not echoed back in the response" and "The refusal reason is logged at WARN with the scheme only" — and it re-creates the credential-in-log leak the PR is filed to close, just on the refusal branch instead of the success branch. 6. Three of the five allowlist entries are unreachable. Because the parse requires a literal 7. The new tests don't establish what they claim8. The "no events emitted" assertion is vacuous — val response = service.navigate(req) // <- emission (if any) happens here
val first = withTimeoutOrNull(500) { service.onNavigationEvent(...).first() }
assertNull(first)
9. The redaction test asserts a pure function, not the log path ( 10. Coverage lost vs. the deleted file, with no replacement: What I'd suggestThe fix for #911 is already on If there's something specific here that One process note: I couldn't compute a merge base ( |
|
Thanks for pushing on the navigation-gate hardening. While this was in flight, the #911 fix landed on dev through #931 in a stronger form, and a head-to-head read says dev wins on every axis:
Closing as superseded by #931 (with #1591 tracking the remaining delta). If there is something specific dev is still missing here, the productive shape is a small PR on top of current dev keeping NAVIGABLE_SCHEMES, authorityRefusal and LogSanitizer.describeUri in place - happy to review that quickly. Appreciate the work that went into this. |
Description
Fixes #911
The browser service (plugin-exposed IPC surface) accepted any non-blank string as a navigation URL:
javascript:executed with no approval surface —navigateran the URL as-is, so ajavascript:alert(document.domain)call executed script in the page context while the MCP/workspace toolchain's CRITICAL approvals never saw it, and a fabricatedCOMPLETEDnavigation event was emitted on top.https://user:Secret123@host/landed in the browser-service log file with credentials intact (fix(logging): redact credentials embedded in a URL's userinfo before logging #640 added the redaction helper; this log path never adopted it).reload()stranded the window loading forever — it setisLoading=trueand emittedSTARTED, but nothing ever completed the cycle, sogetPageInforeportedloadingindefinitely.The fix:
http,https,file,ftp,about) — a refused scheme short-circuits before any state is touched, no events are emitted, and the raw URL is not echoed back in the response. The refusal reason is logged at WARN with the scheme only.redactUrlForLog(the fix(logging): redact credentials embedded in a URL's userinfo before logging #640 userinfo-redaction shape) so credentials never reach the log file.reload()completes its own cycle (STARTED then COMPLETED) and clearsisLoading, since this service's reload is bookkeeping-only and the composeApp engine owns the real reload.🔄 Type of Change
📦 Version Impact
✅ Testing Checklist
🧪 Local Testing
BrowserServiceImplNavigateTest:javascript:URL refused, no navigation events emitted (first-emission probe with a bounded window — no unbounded flow collection)data:URL refusedhttp/httpsstill navigate unchangedreloadcompletes and no longer strandsisLoading=true🖥️ Platform Testing
🏗️ Build Verification
:boss-app-browsercompile + tests + detekt + ktlint green locally🔍 Code Quality
navigationRefusalis extracted as a pure guard —navigatestays under the LongMethod thresholdnavigate's STARTED/COMPLETED event contract instead of inventing a third shape