Skip to content

Present user with option to proceed anyway on invalid SSL cert error - #3711

Merged
austinywang merged 58 commits into
manaflow-ai:mainfrom
deftdawg:certificate-error-bypass-action
Jun 29, 2026
Merged

austinywang merged 58 commits into
manaflow-ai:mainfrom
deftdawg:certificate-error-bypass-action

Conversation

@deftdawg

@deftdawg deftdawg commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Similar to #1666; possibly fixes #2035

  • What changed?
    User is presented a "Proceed Anyway" button when a certificate error occurs

  • Why?
    Self-signed certs and certificates signed by corporate CAs that aren't trusted by OS trust stores prevent CMUX's browser from being useful in dev scenarios and corporate environments.

Testing

EDIT: I've been dog-fooding this for over a month and it works exactly as one would expect.

Demo Video

For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).

  • Video URL or attachment:

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by cubic

Adds a “Proceed Anyway” option for invalid SSL/TLS errors. Uses a one‑time token to safely replay the failed HTTPS request, keeps the failed URL visible in tabs, popups, and during recovery, hides error pages from history/search, and only shows Reload for headerless GETs.

  • New Features

    • Show “Proceed Anyway (Unsafe)” only for invalid cert errors and NSURLErrorSecureConnectionFailed. Tokens are sent via cmuxSSLTrustBypass (fallback cmux-browser-action://bypass-ssl?token=...) and accepted only when the HTTPS scope (host/port) and leaf cert SHA‑256 match; tokens expire after 24h.
    • Preserve and replay the original URLRequest when safe: require the same URL (normalized), method/body/headers; reject streamed bodies, bodyless non‑idempotent replays, and bodies >1 MB. Allow bypass after safe redirects using the final failed URL/scope/fingerprint.
    • Error interstitial loads from an opaque origin, shows the failed URL in the omnibar and popup label, stays out of history/favicons/search. Reload is shown only for headerless GET requests. Localized copy added.
  • Refactors

    • Scope trust‑bypass state per navigation delegate (including popups). Gate the cmuxSSLTrustBypass bridge to the active main‑frame error page and synchronously accept tokens only when a pending token exists (safe Main‑Actor hop).
    • Split error retry into URL‑only, full request replay, or disabled. Clear attempted requests and pending tokens on external routing, downloads, new‑tab opens, and on commit/finish; clear observed SSL fingerprints on new navigation; clear SSL error state after a successful retry. Reset trust grants when the browser context or website data store/profile changes.
    • Split browser display URL helpers and use a restorable display URL for omnibar/blank‑page checks across main windows, popups, and during recovery. Add unit tests for URL normalization and replay‑shape matching.

Written for commit d9a0d39. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • SSL/TLS error pages now show a conditional "Proceed Anyway (Unsafe)" button when available.
    • Bypass attempts use one-time, time-limited tokens; successful bypass remembers the site for subsequent visits. Expired/invalid tokens still allow a reload attempt.
    • Error pages auto-reload shortly before a bypass token expires to simplify retrying.
  • Style

    • Updated error-page layout and button styling (light/dark variants).

Review Change Stack

@vercel

vercel Bot commented May 8, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a thread-safe in-memory SSL bypass store, detects SSL certificate/secure-connection failures, renders a one-time-token-backed “Proceed Anyway (Unsafe)” button on SSL error pages (with auto-reload), intercepts the bypass action to consume the token and record the host, and uses the store to complete server-trust authentication challenges.

Changes

SSL/TLS Error Bypass

Layer / File(s) Summary
SSL bypass store data structure
Sources/Panels/BrowserPanel.swift
BrowserSSLErrorBypassStore singleton maintains a thread-safe locked set of lowercased hostnames and issues single-use, expiring host-bound bypass tokens.
Server-trust authentication with bypass lookup
Sources/Panels/BrowserPanel.swift
WKNavigationDelegate authentication challenge handler checks the bypass store and, when matched, completes NSURLAuthenticationMethodServerTrust challenges with URLCredential(trust:).
Error classification and SSL flag
Sources/Panels/BrowserPanel.swift
Adds isSSLError flag and refactors loadErrorPage to mark specific certificate/secure-connection NSURLError* cases as SSL errors and clear it for other handled connection errors.
Token minting, bypass UI and auto-reload injection
Sources/Panels/BrowserPanel.swift
When isSSLError and the failed URL has a host, mints a pending token, builds a cmux-browser-action://bypass-ssl?token=...&url=... bypass link, and injects an auto-reload script timed before token expiry; omitted for non-SSL errors.
Error-page styling and layout changes
Sources/Panels/BrowserPanel.swift
Adds button.bypass CSS with dark/light overrides and updates the error-page HTML to place reload and bypass controls together and append the conditional auto-reload script.
Bypass URL scheme interception and store update
Sources/Panels/BrowserPanel.swift
Navigation policy intercepts cmux-browser-action://bypass-ssl, parses token and url, consumes the pending token for the target host, records bypass eligibility if valid, and then loads the target URL regardless of token validity.

Sequence Diagram

sequenceDiagram
  participant User as User/Browser
  participant Nav as NavigationDelegate
  participant Store as BrowserSSLErrorBypassStore
  participant Auth as AuthChallengeHandler
  User->>Nav: Clicks "Proceed Anyway (Unsafe)" (cmux-browser-action://bypass-ssl?token=&url=)
  Nav->>Nav: parse `token` and `url`
  Nav->>Store: consumePendingToken(token, forHost:)
  Store-->>Nav: token valid / token invalid/expired
  Nav->>Store: add(host)  // when token valid
  Nav->>Nav: load(target URL)
  Note right of Auth: Later, system presents server-trust challenge
  Auth->>Store: check(host)
  Store-->>Auth: host found / not found
  Auth->>Auth: completeChallenge with URLCredential(trust:) if host found
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#806: Modifies the same WKNavigationDelegate server-trust authentication challenge handling in BrowserPanel.swift, making these PRs directly related in authentication challenge code paths.

Poem

🐰 I sniffed a crooked TLS trail,
A single token, fragile sail,
Click "unsafe" — the host I pen,
The load resumes, then hops again,
A tiny hop for a rabbit's tale.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error Line 6416 exposes error.localizedDescription (raw system message) as user-facing error text, violating the rule against exposing "raw upstream messages". Replace error.localizedDescription with a generic localized message (e.g., String(localized: "browser.error.unknown", defaultValue: "An error occurred."))
Cmux Architecture Rethink ❌ Error PR introduces BrowserSSLErrorBypassStore using NSLock for shared-state and setTimeout-based token refresh. Violates the architectural rule against locks and timing repairs for shared-state races. Redesign bypass state with a single owner and no locks. Make tokens persistent or non-expiring to eliminate setTimeout-based refresh. Current approach patches symptoms rather than fixing root cause.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Auxiliary Window Close Shortcuts ❓ Inconclusive The referenced rule file .github/review-bot-rules/swift-auxiliary-window-close-shortcuts.md does not exist in the repository, as verified by directory listing and README inventory. Rule file is missing. If needed, create the rule. If not needed, remove the check. The PR adds no new standalone windows—only a data store and error page modifications.
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: presenting users with a bypass option when SSL certificate errors occur.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed @unchecked Sendable with lock-guarded access; documented safety justification referencing the actor isolation rule for sync delegate callbacks.
Cmux Swift Blocking Runtime ✅ Passed NSLock for synchronous WKNavigationDelegate callbacks. Well-documented (lines 842-850). Holds O(1) operations only. Allowed: low-level bridge when actor not viable. No sleep/semaphore.
Cmux No Hacky Sleeps ✅ Passed PR only modifies Swift code (BrowserPanel.swift). The check explicitly applies to TypeScript, JavaScript, shell, or build/runtime scripts only. Swift is explicitly excluded per rule scope.
Cmux Swift Concurrency ✅ Passed Proper NSLock synchronization for WKKit callback boundary. No Dispatch queues, Combine state, or fire-and-forget Tasks introduced for SSL error bypass feature.
Cmux Swift @Concurrent ✅ Passed All new Swift code is synchronous. BrowserSSLErrorBypassStore uses @unchecked Sendable with NSLock protection. No async functions, @concurrent annotations, or UI isolation violations.
Cmux Swift File And Package Boundaries ✅ Passed Adds 162 lines to 10,768-line file (below 250 threshold). BrowserSSLErrorBypassStore has focused responsibility. Matches allowed exception for focused features on large files.
Cmux Swift Logging ✅ Passed The PR adds SSL bypass functionality with no new logging violations. All 17 NSLog statements in BrowserPanel.swift are existing code unrelated to the SSL bypass features.
Cmux Swiftui State Layout ✅ Passed File has no SwiftUI import. BrowserSSLErrorBypassStore is not SwiftUI state. No new @Published properties. Token creation in WKWebView delegate callback, not render.
Description check ✅ Passed The PR follows the required template and includes summary, testing, review trigger, and checklist sections; only the demo video field is empty.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deftdawg
deftdawg marked this pull request as ready for review May 14, 2026 17:28
@deftdawg

Copy link
Copy Markdown
Contributor Author
Proceed Anyway (Unsafe)

Finally was able to test locally and it works wonderfully... Hopefully we can get it merged.

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@cubic-dev-ai

cubic-dev-ai Bot commented May 14, 2026

Copy link
Copy Markdown
Proceed Anyway (Unsafe)

Finally was able to test locally and it works wonderfully... Hopefully we can get it merged.

@codex review
...

@deftdawg I have started the AI code review. It will take a few minutes to complete.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

@deftdawg The Comment image shows a dark-themed SSL error dialog with:

  • Title: "Connection isn't secure"
  • Subtitle: "The certificate for this site is invalid."
  • A redacted URL (ending in 1646/ui/jobs)
  • Two buttons: a "Reload" button (gray/dark) and a "Proceed Anyway (Unsafe)" button (red/orange outlined)

The UI looks great — the warning color on "Proceed Anyway (Unsafe)" clearly communicates the security implications to the user while still making the action accessible. The visual distinction between the safe ("Reload") and unsafe action is well done. I'll kick off a full review of the PR now!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a "Proceed Anyway (Unsafe)" button to the browser's SSL error interstitial, enabling users to bypass self-signed or corporate-CA certificates that macOS doesn't trust. Bypass tokens are one-time UUIDs tied to the observed leaf-cert SHA-256 and the exact failed URLRequest, scoped per navigation delegate instance, and expire after 24 hours.

  • Adds BrowserSSLTrustBypassState (@MainActor, per-delegate lifetime) with token issuance, fingerprint-matching, LRU eviction, and request replay, all well-tested in BrowserSSLTrustBypassStateTests.swift.
  • Refactors the error page into BrowserErrorPage/BrowserErrorPageContent/BrowserErrorPageRetry, using a data-token attribute and static onclick script to avoid JS-string injection; threads activeErrorPageDisplayURL through all display-URL helpers in BrowserPanel.
  • Duplicates the entire bypass state machine (~250 lines) into PopupUIDelegate in BrowserPopupWindowController.swift, which has already diverged from BrowserNavigationDelegate in didCommit handling; a shared coordinator type would eliminate this maintenance risk.

Confidence Score: 4/5

The core bypass logic is well-designed and safe; the main concern is that the entire state machine is duplicated into PopupUIDelegate, and the two copies have already diverged in didCommit handling.

The token, fingerprint, and request-replay machinery in BrowserSSLTrustBypassState is thoroughly tested and correctly scoped. The one structural issue is that PopupUIDelegate manually re-implements ~250 lines of bypass state that belong in a shared coordinator — and this duplication has already produced a real behavioral difference in didCommit (the popup clears lastAttemptedRequest on every non-error-page commit while the main delegate defers to BrowserPanel.publishCommittedURL). A future bug fix or invariant change applied to BrowserNavigationDelegate will silently miss the popup path unless the author remembers to mirror it.

Sources/Panels/BrowserPopupWindowController.swift — the PopupUIDelegate class duplicates the SSL bypass state machine from BrowserNavigationDelegate with a subtle didCommit divergence; both files need to be updated together for any future bypass-state fix.

Important Files Changed

Filename Overview
Sources/Panels/BrowserErrorPage.swift New file: extracts error-page HTML generation. Token is stored in a data-* attribute and read via dataset.token, avoiding the JS-string injection risk from previous iterations. escapeHTML covers all attribute-breaking chars. Retry and bypass paths are cleanly separated.
Sources/Panels/BrowserSSLTrustBypassState.swift New @MainActor class scoping bypass grants per navigation delegate. Token lifetime, fingerprint matching, request size limits, and LRU eviction are all well-modelled. maximumPendingBypassCount is reused as the bound for three separate collections (tokens, fingerprints, grants), which is a minor naming clarity issue.
Sources/Panels/BrowserNavigationDelegate.swift Adds SSL bypass state tracking and request recording/replay logic. didCommit clears state for bypass/retry paths; normal navigation state cleanup is delegated to BrowserPanel.publishCommittedURL — this asymmetry relative to PopupUIDelegate works but relies on BrowserPanel's extra coordination step.
Sources/Panels/BrowserPopupWindowController.swift Entire SSL bypass state machine (~250 lines) is duplicated verbatim from BrowserNavigationDelegate into PopupUIDelegate. The duplication has already diverged: popup's didCommit has an extra else if branch absent from the main delegate. Architectural rethink needed.
Sources/Panels/BrowserSSLTrustBypassMessageHandler.swift New WKScriptMessageHandler; validates token as UUID string before dispatching. Uses MainActor.assumeIsolated synchronously to prevent unbounded main-actor work queuing. Token handling is gated by the owning delegate's canHandleToken closure.
Sources/Panels/URLRequest+BrowserFailedNavigation.swift New extension: URL normalization and replay-shape matching. Header comparison is case-insensitive (lowercased keys). browserCanReloadWithURLOnly correctly gates Reload to headerless GET-only requests.
Sources/Panels/BrowserPanel.swift Wires up BrowserSSLTrustBypassMessageHandler, threads activeErrorPageDisplayURL through all display-URL resolution paths, and gates history/favicon updates behind error-page state. publishCommittedURL early-returns for error pages without calling clearAttemptedRequest, relying on didCommit to have already cleared for replay paths.
cmuxTests/BrowserSSLTrustBypassStateTests.swift Comprehensive test suite covering token issuance, expiry, fingerprint matching, request replay shape, URL normalization, body/stream size limits, LRU eviction, and state reset. Good coverage of all key security invariants.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant WKWebView
    participant NavDelegate as BrowserNavigationDelegate
    participant BypassState as BrowserSSLTrustBypassState
    participant ErrorPage as BrowserErrorPage
    participant MsgHandler as BrowserSSLTrustBypassMessageHandler

    User->>WKWebView: Navigate to https://self-signed.internal
    WKWebView->>NavDelegate: decidePolicyFor(navigationAction)
    NavDelegate->>NavDelegate: recordAttemptedRequest(request)
    WKWebView->>NavDelegate: didReceive(challenge: ServerTrust)
    NavDelegate->>BypassState: recordObservedServerTrust(trust, scope)
    NavDelegate->>WKWebView: .performDefaultHandling (rejects cert)
    WKWebView->>NavDelegate: didFailProvisionalNavigation(error)
    NavDelegate->>ErrorPage: load(failedURL, retry, sslBypassState)
    ErrorPage->>BypassState: createPendingBypassAction(request)
    BypassState-->>ErrorPage: "cmux-browser-action://bypass-ssl?token=UUID"
    ErrorPage->>WKWebView: loadHTMLString (with data-token button)
    NavDelegate->>NavDelegate: "acceptsSSLTrustBypassMessages = true"

    User->>WKWebView: Click Proceed Anyway
    WKWebView->>MsgHandler: userContentController(didReceive token)
    MsgHandler->>NavDelegate: canHandleSSLTrustBypassToken(token)
    NavDelegate->>MsgHandler: true
    MsgHandler->>NavDelegate: handleSSLTrustBypassToken(token, webView)
    NavDelegate->>BypassState: consumePendingBypassToken(token)
    BypassState-->>NavDelegate: original URLRequest + registers bypass grant
    NavDelegate->>WKWebView: load(originalRequest)
    WKWebView->>NavDelegate: didReceive(challenge: ServerTrust)
    NavDelegate->>BypassState: isBypassed(scope, fingerprint)?
    BypassState-->>NavDelegate: true
    NavDelegate->>WKWebView: .useCredential (trust accepted)
    WKWebView->>NavDelegate: didCommit then clearAttemptedRequest()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant WKWebView
    participant NavDelegate as BrowserNavigationDelegate
    participant BypassState as BrowserSSLTrustBypassState
    participant ErrorPage as BrowserErrorPage
    participant MsgHandler as BrowserSSLTrustBypassMessageHandler

    User->>WKWebView: Navigate to https://self-signed.internal
    WKWebView->>NavDelegate: decidePolicyFor(navigationAction)
    NavDelegate->>NavDelegate: recordAttemptedRequest(request)
    WKWebView->>NavDelegate: didReceive(challenge: ServerTrust)
    NavDelegate->>BypassState: recordObservedServerTrust(trust, scope)
    NavDelegate->>WKWebView: .performDefaultHandling (rejects cert)
    WKWebView->>NavDelegate: didFailProvisionalNavigation(error)
    NavDelegate->>ErrorPage: load(failedURL, retry, sslBypassState)
    ErrorPage->>BypassState: createPendingBypassAction(request)
    BypassState-->>ErrorPage: "cmux-browser-action://bypass-ssl?token=UUID"
    ErrorPage->>WKWebView: loadHTMLString (with data-token button)
    NavDelegate->>NavDelegate: "acceptsSSLTrustBypassMessages = true"

    User->>WKWebView: Click Proceed Anyway
    WKWebView->>MsgHandler: userContentController(didReceive token)
    MsgHandler->>NavDelegate: canHandleSSLTrustBypassToken(token)
    NavDelegate->>MsgHandler: true
    MsgHandler->>NavDelegate: handleSSLTrustBypassToken(token, webView)
    NavDelegate->>BypassState: consumePendingBypassToken(token)
    BypassState-->>NavDelegate: original URLRequest + registers bypass grant
    NavDelegate->>WKWebView: load(originalRequest)
    WKWebView->>NavDelegate: didReceive(challenge: ServerTrust)
    NavDelegate->>BypassState: isBypassed(scope, fingerprint)?
    BypassState-->>NavDelegate: true
    NavDelegate->>WKWebView: .useCredential (trust accepted)
    WKWebView->>NavDelegate: didCommit then clearAttemptedRequest()
Loading

Reviews (43): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes May 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 6388-6390: The current bypass button HTML (bypassButtonHTML) only
round-trips the URL, causing POSTs/headers/body to be lost; change the flow to
serialize and persist the original URLRequest (including HTTP method, headers,
and body) behind a short-lived bypass token instead of passing url=..., update
the button to call "cmux-browser-action://bypass-ssl?token=<token>" (or include
token param alongside escapedURL), and modify the handler for
"cmux-browser-action://bypass-ssl" to: mark the host as bypassed, look up and
deserialize the stored URLRequest for that token, and replay the exact original
request (e.g., via webView.load(request:) or equivalent) so method/headers/body
are preserved; apply the same change to the other similar block referenced
(lines 6448-6457).
- Around line 6448-6459: The bypass-ssl handler currently honors any
cmux-browser-action request; update the logic in the navigationAction handling
block (the if that checks url.scheme == "cmux-browser-action" && url.host ==
"bypass-ssl") to require a one-time nonce/token: when rendering the internal SSL
error page generate and store a pending token in the same store that tracks
pending bypasses (e.g., add a pendingBypassToken in BrowserSSLErrorBypassStore
or the SSL error page renderer), require the incoming query to include url=...
and token=... and only call BrowserSSLErrorBypassStore.shared.addBypass(for:)
and webView.load(...) if the token matches the stored pending token;
consume/clear the token immediately after use and reject
(decisionHandler(.cancel)) if missing, mismatched, expired, or already-consumed.
Ensure token creation happens when the SSL error page is shown and that tokens
are single-use and time-limited.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a92f4fc-92f8-4d8d-91cf-f4c695e9a470

📥 Commits

Reviewing files that changed from the base of the PR and between 7142e31 and 541a060.

📒 Files selected for processing (1)
  • Sources/Panels/BrowserPanel.swift

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
@greptile-apps

greptile-apps Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a "Proceed Anyway (Unsafe)" button to the SSL/TLS error page in the in-app browser, backed by a new BrowserSSLErrorBypassStore that tracks per-host bypasses and a new cmux-browser-action://bypass-ssl URL-scheme handler that records the bypass and reloads the target URL.

  • The BrowserSSLErrorBypassStore singleton uses NSLock over a plain class, violating the project's rule that shared mutable state should use an actor; the class also lacks actor isolation or Sendable conformance, creating a Swift 6 isolation hole on every static let shared access.
  • The bypass button's onclick embeds the HTML-escaped URL directly inside a JavaScript single-quoted string literal; because single quotes are not escaped, a URL containing ' can inject and execute arbitrary JavaScript in the WKWebView, reachable via the custom cmux-browser-action:// scheme handler.

Confidence Score: 3/5

Not safe to merge as-is: the bypass button injects the failed URL into a JavaScript string without escaping single quotes, which allows JS execution in the WKWebView when navigating to a crafted URL.

The SSL error page embeds escapedURL verbatim inside a JS single-quoted string literal, making it straightforward for any URL with a ' character to break out of the string and execute code in the WKWebView context. Separately, BrowserSSLErrorBypassStore uses an NSLock-protected class rather than an actor, which leaves the shared bypass state without compiler-enforced isolation and creates a Swift 6 concurrency hole on every access to static let shared.

Sources/Panels/BrowserPanel.swift — both the loadErrorPage HTML generation and the new BrowserSSLErrorBypassStore type.

Security Review

  • JS string injection (XSS) in BrowserPanel.swift (loadErrorPage): the escapedURL value is interpolated into a JavaScript single-quoted string literal without escaping single quotes. A URL with an embedded ' can terminate the string early, and a crafted URL can execute arbitrary JavaScript in the WKWebView when the SSL error page loads, triggering the app's custom cmux-browser-action:// scheme handler.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel.swift Adds SSL-bypass UI and a new BrowserSSLErrorBypassStore singleton. Contains a JS string-injection vulnerability in the bypass button's onclick and uses NSLock instead of an actor for the shared bypass store.

Reviews (2): Last reviewed commit: "Update Sources/Panels/BrowserPanel.swift" | Re-trigger Greptile

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
… error; function requires a one-time token to prevent sites from being able to spoof user acceptance.
@deftdawg
deftdawg force-pushed the certificate-error-bypass-action branch from 2a936a4 to 2c69776 Compare May 18, 2026 03:16
coderabbitai[bot]
coderabbitai Bot previously requested changes May 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Panels/BrowserPanel.swift (1)

6411-6434: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Avoid injecting failedURL into inline JavaScript.

Line 6433 drops the HTML-escaped URL into a single-quoted JS string. A certificate-failing URL containing ' can break the handler and run attacker-controlled script in this internal page. Precompute the deep link with URLComponents and inject only the encoded result.

🔒 Suggested change
         if isSSLError,
            let failedHost = URL(string: failedURL)?.host {
             let store = BrowserSSLErrorBypassStore.shared
             let token = store.createPendingToken(for: failedHost)
-            let escapedToken = escapeHTML(token)
+            var bypassComponents = URLComponents()
+            bypassComponents.scheme = "cmux-browser-action"
+            bypassComponents.host = "bypass-ssl"
+            bypassComponents.queryItems = [
+                URLQueryItem(name: "token", value: token),
+                URLQueryItem(name: "url", value: failedURL),
+            ]
+            let escapedBypassURL = escapeHTML(bypassComponents.string ?? "")
             bypassButtonHTML = """
-                <button class="bypass" onclick="window.location.href='cmux-browser-action://bypass-ssl?token=\(escapedToken)&url=' + encodeURIComponent('\(escapedURL)')">\(escapedBypassLabel)</button>
+                <button class="bypass" onclick="window.location.href='\(escapedBypassURL)'">\(escapedBypassLabel)</button>
             """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Panels/BrowserPanel.swift` around lines 6411 - 6434, The bypass
button HTML is vulnerable because failedURL is injected into a single-quoted JS
string; instead build the deep link URL server-side using URLComponents (use the
existing BrowserSSLErrorBypassStore.shared.createPendingToken(for: failedHost)
for token and failedHost), percent‑encode the url parameter via
URLComponents/addingPercentEncoding, then pass that fully encoded deep link into
bypassButtonHTML (replace the inline "' + encodeURIComponent(...)" approach) and
only run escapeHTML on the final deep link string before interpolating it into
the button HTML; update the bypassButtonHTML construction in the same block that
defines escapedToken and failedHost to use the precomputed encoded deep link.
♻️ Duplicate comments (1)
Sources/Panels/BrowserPanel.swift (1)

6514-6532: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replay the original request after bypass.

Lines 6526-6532 rebuild the retry with URLRequest(url:), so any SSL-failed POST, custom headers, or body are retried as a plain GET. Persist the original URLRequest behind the token and webView.load(...) that exact request after addBypass(for:).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Panels/BrowserPanel.swift` around lines 6514 - 6532, The retry
currently rebuilds the request as URLRequest(url:) losing method, headers and
body; update the flow to persist the original URLRequest when creating/issuing
the bypass token and, in BrowserPanel where you call
BrowserSSLErrorBypassStore.shared.consumePendingToken(token, for: host) and then
addBypass(for:), load the original persisted URLRequest (not a new
URLRequest(url:)) via webView.load(originalRequest) so POSTs and custom
headers/bodies are replayed intact; modify BrowserSSLErrorBypassStore to
accept/store the full URLRequest keyed to the token and provide a retrieval API
used here.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 6405-6408: The default branch currently assigns
error.localizedDescription to message (in the switch handling page open errors),
which exposes raw upstream/vendor details; replace that assignment with a
generic localized fallback string (e.g., a localized key like
"browser.error.cantOpen.message" or similar) and set isSSLError = false as
before, and if needed log the original error (error or
error.localizedDescription) to the console/logger rather than showing it to the
user; update the code around the default case where title, message, and
isSSLError are set to implement this change.

---

Outside diff comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 6411-6434: The bypass button HTML is vulnerable because failedURL
is injected into a single-quoted JS string; instead build the deep link URL
server-side using URLComponents (use the existing
BrowserSSLErrorBypassStore.shared.createPendingToken(for: failedHost) for token
and failedHost), percent‑encode the url parameter via
URLComponents/addingPercentEncoding, then pass that fully encoded deep link into
bypassButtonHTML (replace the inline "' + encodeURIComponent(...)" approach) and
only run escapeHTML on the final deep link string before interpolating it into
the button HTML; update the bypassButtonHTML construction in the same block that
defines escapedToken and failedHost to use the precomputed encoded deep link.

---

Duplicate comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 6514-6532: The retry currently rebuilds the request as
URLRequest(url:) losing method, headers and body; update the flow to persist the
original URLRequest when creating/issuing the bypass token and, in BrowserPanel
where you call BrowserSSLErrorBypassStore.shared.consumePendingToken(token, for:
host) and then addBypass(for:), load the original persisted URLRequest (not a
new URLRequest(url:)) via webView.load(originalRequest) so POSTs and custom
headers/bodies are replayed intact; modify BrowserSSLErrorBypassStore to
accept/store the full URLRequest keyed to the token and provide a retrieval API
used here.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a23d4a5a-39c0-4648-84d9-57aa0abbb22c

📥 Commits

Reviewing files that changed from the base of the PR and between 2a936a4 and 2c69776.

📒 Files selected for processing (1)
  • Sources/Panels/BrowserPanel.swift

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Panels/BrowserPanel.swift">

<violation number="1" location="Sources/Panels/BrowserPanel.swift:6538">
P1: Restrict `targetURL` to `http`/`https` before loading it. The current guard accepts any URL with a host, so a crafted `cmux-browser-action://bypass-ssl?...` request can drive `webView.load` for non-web schemes (for example `file://`) even when token validation fails.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

Comment thread Sources/Panels/BrowserPanel.swift Outdated
@deftdawg

Copy link
Copy Markdown
Contributor Author

Manually tested using sites from https://badssl.com/, ready for human review.

@deftdawg

deftdawg commented May 26, 2026 •

Copy link
Copy Markdown
Contributor Author

@lawrencecchen Can you let me know if there's anything else I need to get this merged? (thx)

@deftdawg

deftdawg commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor Author

@lawrencecchen @austinywang This feature (to allow users to proceed anyway if an SSL cert is invalid) is ready to merge, please let me know if there's anything else I can do to help to get it merged. Thanks. 🙏

Comment on lines +81 to +82
if activeSSLTrustBypassReplayRequest != nil {
clearAttemptedRequest(discardPendingBypasses: true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Clear successful retries This only clears the interstitial state for the token replay path. A user can press Reload on the SSL error page, WebKit retries the same request through the preserved .other navigation path, and the retried page can then commit with activeSSLTrustBypassReplayRequest == nil. In that state activeErrorPageDisplayURL is left set, so BrowserPanel.publishCommittedURL keeps treating the real page as an error page: the omnibar stays on the failed URL and history/favicon updates stay skipped. The popup delegate has the same state transition, so the successful retry path needs to clear the active error-page state when the real navigation commits, not only when a bypass replay commits.

@austinywang
austinywang dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] June 26, 2026 18:09

Stale CodeRabbit change request against a superseded commit; current CodeRabbit check passes and addressed feedback is resolved.

@vercel

vercel Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 29, 2026 6:01am

@austinywang
austinywang merged commit 1f9e241 into manaflow-ai:main Jun 29, 2026
27 of 29 checks passed
@deftdawg
deftdawg deleted the certificate-error-bypass-action branch June 29, 2026 13:45
austinywang added a commit that referenced this pull request Jun 29, 2026
Resolve conflicts after main advanced (SSL "proceed anyway" #3711, etc.):
- BrowserNavigationDelegate.swift (real code merge):
  * didStartProvisionalNavigation: keep main's lastAttemptedRequest URL
    fallback plus this branch's print-reset + didClearPDFDocument().
  * shouldPerformDownload: keep this branch's subframe download gating
    (main-frame / user-activation / recorded-intent / insecure-HTTP
    block) and add main's clearAttemptedRequest() on the path that
    actually proceeds to .download.
  * keep this branch's WKWebView.cmuxRunPrintOperation() extension.
- cmux.xcodeproj/project.pbxproj: union of both branches' new files
  (main's SSL/error-page files + this branch's PDF/popup-policy files),
  re-normalized; no duplicate entries.
- .github/swift-file-length-budget.tsv: regenerated from merged tree.

Localizable.xcstrings auto-merged. Swift compiles clean (tagged Debug
build SUCCEEDED).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — d9a0d392 Deployed Jun 29, 2026 by vercel[bot]
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.

WKWebView rejects self-signed / invalid SSL certificates with no user bypass option

2 participants