Skip to content

fix(ios): add cookie handling to proxy scheme handler#463

Open
WcaleNieWolny wants to merge 3 commits intofeature/proxy-redesignfrom
fix/proxy-cookie-handling
Open

fix(ios): add cookie handling to proxy scheme handler#463
WcaleNieWolny wants to merge 3 commits intofeature/proxy-redesignfrom
fix/proxy-cookie-handling

Conversation

@WcaleNieWolny
Copy link
Contributor

Summary

  • Fixes cookie store mismatch between WKWebView and URLSession in the proxy pass-through path
  • Prevents httpShouldHandleCookies duplicate/conflict issues by disabling URLSession's cookie management
  • Syncs Set-Cookie headers from pass-through responses back to WKHTTPCookieStore
  • Handles redirect cookie chains via delegate-based URLSession with cookie injection per redirect step

Root cause

WKWebView has its own cookie store (WKHTTPCookieStore), separate from URLSession's HTTPCookieStorage.shared. When executePassThrough proxied requests through URLSession.shared:

  1. URLSession could add duplicate cookies from its own jar
  2. Set-Cookie headers from responses were never synced back to WKWebView
  3. Redirect responses that set cookies were lost — the redirect target never received them

This broke any site relying on session cookies through pass-through (e.g., Google reCAPTCHA).

Changes

Fix What it does
httpShouldHandleCookies = false Prevents URLSession from adding its own cookies — WKWebView already put them in request headers
syncCookies() helper Extracts Set-Cookie from HTTPURLResponse and stores them in WKHTTPCookieStore with async completion
Delegate-based URLSession Replaces completion-handler dataTask so willPerformHTTPRedirection fires
Redirect cookie handling At each redirect: sync Set-Cookie to WKWebView, then inject WKWebView cookies for the redirect target domain
pendingWebViews tracking Stores WKWebView reference per request for cookie sync access
invalidateSession() Breaks the URLSession ↔ delegate retain cycle when handler is disposed

Test plan

  • Open in-app browser with proxy pass-through to a site that sets cookies (e.g., Google)
  • Verify session cookies persist across page loads
  • Verify redirect chains (e.g., Google sign-in flow) maintain cookies between domains
  • Verify reCAPTCHA loads and functions correctly
  • Verify existing proxy interception (non-pass-through) still works

WKWebView's cookie store and URLSession's cookie jar are separate.
When the scheme handler proxies requests via URLSession, Set-Cookie
headers from responses are not automatically synced back to WKWebView,
breaking session cookies (e.g. Google reCAPTCHA).

Three fixes:
1. Set httpShouldHandleCookies=false to prevent URLSession from adding
   its own cookies (WKWebView already includes them in request headers)
2. Sync Set-Cookie headers from pass-through responses back to
   WKWebView's WKHTTPCookieStore before delivering the response
3. Use delegate-based URLSession to intercept redirects — sync cookies
   from each redirect response and inject WKWebView cookies for the
   redirect target domain, ensuring redirect chains maintain sessions
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b244efa-df6a-49f4-9b2a-2ea3d96b82ff

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/proxy-cookie-handling

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8129abba29

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +316 to +320
if !matchingCookies.isEmpty {
let headers = HTTPCookie.requestHeaderFields(with: matchingCookies)
for (key, value) in headers {
modifiedRequest.setValue(value, forHTTPHeaderField: key)
}

Choose a reason for hiding this comment

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

P1 Badge Clear Cookie header when no redirect cookies match

When matchingCookies is empty, the code leaves modifiedRequest’s existing Cookie header untouched, so a redirect to another host can forward cookies from the previous hop instead of sending none. This is a real cross-domain leakage path because newRequest may carry the original Cookie header; in that case, redirects without target-domain cookies still send source-domain cookies to the new origin.

Useful? React with 👍 / 👎.

Comment on lines +307 to +311
let matchingCookies = allCookies.filter { cookie in
guard let host = redirectURL.host else { return false }
let domain = cookie.domain
if host == domain { return true }
if domain.hasPrefix(".") {

Choose a reason for hiding this comment

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

P2 Badge Filter redirect cookies with full URL rules

The redirect cookie selection only checks domain/host and then serializes all matches into Cookie, which ignores important cookie constraints like Path and Secure. This can attach cookies that WKWebView would not send (for example, /app cookies on unrelated paths or secure cookies on non-HTTPS redirects), causing auth inconsistencies and potential cookie exposure.

Useful? React with 👍 / 👎.

Copy link

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 updates the iOS ProxySchemeHandler pass-through path to keep WKWebView’s cookie store (WKHTTPCookieStore) in sync with URLSession requests/responses, including redirects, to avoid cookie duplication and missing Set-Cookie propagation.

Changes:

  • Switches pass-through networking to a delegate-based URLSession and disables URLSession cookie handling (httpShouldHandleCookies = false).
  • Adds Set-Cookie syncing from pass-through (and JS-provided) responses into WKHTTPCookieStore.
  • Adds redirect handling that syncs cookies from redirect responses and injects WKWebView cookies into redirect requests.

💡 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 9 to 11
private var pendingNetworkTasks: [String: URLSessionDataTask] = [:]
private var pendingWebViews: [String: WKWebView] = [:]
private var stoppedRequests: Set<String> = []
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

pendingWebViews stores strong references to WKWebView. Because the WKWebView configuration retains its WKURLSchemeHandler, this can create a temporary retain cycle (and can become a leak if a request never completes/cleanup isn’t called). Consider storing WKHTTPCookieStore / WKWebsiteDataStore instead of the full WKWebView, or use a weak wrapper for the webview reference.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +93
@@ -75,6 +90,7 @@ public class ProxySchemeHandler: NSObject, WKURLSchemeHandler {
return
}
self.pendingBodies.removeValue(forKey: requestId)
self.pendingWebViews.removeValue(forKey: requestId)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

The timeout removes pendingTasks unconditionally. For pass-through, pendingTasks is intentionally kept while the URLSession task runs, so any request taking longer than proxyTimeoutSeconds will be failed even though JS already responded (with nil). Consider cancelling the timeout once handleResponse is called, or skipping timeout failure when a network task is already in-flight for this requestId.

Copilot uses AI. Check for mistakes.
completion()
return
}
let headerFields = response.allHeaderFields as? [String: String] ?? [:]
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

response.allHeaderFields as? [String: String] can fail (header values are often Any/NSNumber), which would drop all cookies and prevent Set-Cookie sync. Consider building a [String: String] dictionary by stringifying keys/values instead of using a conditional cast.

Suggested change
let headerFields = response.allHeaderFields as? [String: String] ?? [:]
var headerFields: [String: String] = [:]
for (key, value) in response.allHeaderFields {
headerFields[String(describing: key)] = String(describing: value)
}

Copilot uses AI. Check for mistakes.
Comment on lines +235 to +237
cookieStore.setCookie(cookie) { group.leave() }
}
group.notify(queue: .global(), execute: completion)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

WKHTTPCookieStore / WKWebView access and the completion for syncCookies are happening off the main queue (group.notify(queue: .global())). Elsewhere in the codebase cookie-store operations are dispatched to .main; WebKit objects aren’t generally thread-safe. Consider dispatching setCookie/getAllCookies and the completion (which triggers urlSchemeTask.didReceive/didFinish) onto DispatchQueue.main.

Suggested change
cookieStore.setCookie(cookie) { group.leave() }
}
group.notify(queue: .global(), execute: completion)
DispatchQueue.main.async {
cookieStore.setCookie(cookie) {
group.leave()
}
}
}
group.notify(queue: .main, execute: completion)

Copilot uses AI. Check for mistakes.
Comment on lines +303 to +323
webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { allCookies in
var modifiedRequest = request
modifiedRequest.httpShouldHandleCookies = false

let matchingCookies = allCookies.filter { cookie in
guard let host = redirectURL.host else { return false }
let domain = cookie.domain
if host == domain { return true }
if domain.hasPrefix(".") {
return host.hasSuffix(domain) || host == String(domain.dropFirst())
}
return false
}
if !matchingCookies.isEmpty {
let headers = HTTPCookie.requestHeaderFields(with: matchingCookies)
for (key, value) in headers {
modifiedRequest.setValue(value, forHTTPHeaderField: key)
}
}
completionHandler(modifiedRequest)
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Manual cookie injection in urlSession(_:willPerformHTTPRedirection:newRequest:completionHandler:) uses getAllCookies and filters only by domain before calling HTTPCookie.requestHeaderFields, which ignores cookie attributes like Secure and path. This can cause Secure cookies to be sent over plain HTTP redirects and path-scoped cookies to be exposed to unrelated paths, enabling session hijacking or cross-app data leakage if an attacker can influence redirects or network traffic. To fix, use the cookie store API that enforces cookie rules for a specific URL and ensure that Secure cookies are only attached to https requests and that path constraints are respected when building the redirect request.

Copilot uses AI. Check for mistakes.
Address Codex review findings:
- P1: Clear Cookie header when no cookies match the redirect target,
  preventing source-domain cookies from leaking to a different host
- P2: Add path and secure attribute checks to cookie filtering,
  not just domain matching
WKWebView.configuration must only be accessed from the main thread.
The URLSession delegate callbacks (redirect handling, completion) run
on a background queue, causing a Main Thread Checker crash when
syncCookies or willPerformHTTPRedirection accessed webView.configuration.

Fix: capture the WKHTTPCookieStore reference in webView(_:start:) which
is guaranteed to run on the main thread, then pass it through instead
of the WKWebView reference. This eliminates all background-thread
access to WKWebView.configuration.
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 4, 2026

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.

2 participants