diff --git a/codex-rs/http-client/src/chatgpt_cloudflare_cookies.rs b/codex-rs/http-client/src/chatgpt_cloudflare_cookies.rs index a2abde31a9..69124e0315 100644 --- a/codex-rs/http-client/src/chatgpt_cloudflare_cookies.rs +++ b/codex-rs/http-client/src/chatgpt_cloudflare_cookies.rs @@ -8,9 +8,9 @@ use reqwest::header::HeaderValue; use crate::chatgpt_hosts::is_allowed_chatgpt_host; // WARNING: this HTTP cookie store is process-global and may be shared across auth contexts. -// It must only ever contain Cloudflare infrastructure cookies. Never extend this -// store to persist ChatGPT account, session, auth, or other user-specific cookie -// data. +// It must only ever contain Cloudflare infrastructure cookies and the `__oailb` +// routing cookie. Never extend this store to persist ChatGPT account, session, +// auth, or other user-specific cookie data. static SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE: LazyLock> = LazyLock::new(|| Arc::new(ChatGptCloudflareCookieStore::default())); @@ -96,12 +96,13 @@ impl CookieStore for ChatGptCookieStore { } } -/// Adds the process-local ChatGPT Cloudflare cookie jar used by Codex HTTP clients. +/// Adds the process-local ChatGPT infrastructure cookie jar used by Codex HTTP clients. /// /// WARNING: this jar is global within the process. It is only acceptable because it hardcodes a -/// small allowlist of Cloudflare cookie names and refuses all other ChatGPT cookies. Do not store -/// ChatGPT account, session, auth, or other user-specific cookies here. If a future caller needs -/// those cookies, the store must be scoped to the auth/session owner instead of shared globally. +/// small allowlist of Cloudflare cookie names plus `__oailb` for routing, and refuses all other +/// ChatGPT cookies. Do not store ChatGPT account, session, auth, or other user-specific cookies +/// here. If a future caller needs those cookies, the store must be scoped to the auth/session +/// owner instead of shared globally. pub fn with_chatgpt_cloudflare_cookie_store( builder: reqwest::ClientBuilder, ) -> reqwest::ClientBuilder { @@ -155,6 +156,7 @@ fn only_cloudflare_cookies(header: HeaderValue) -> Option { } fn is_allowed_cloudflare_cookie_name(name: &str) -> bool { + // `__oailb` is an OpenAI infrastructure routing cookie, not an authentication cookie. // Keep this allowlist aligned with Cloudflare's documented service cookies: // https://developers.cloudflare.com/fundamentals/reference/policies-compliances/cloudflare-cookies/ matches!( @@ -164,6 +166,7 @@ fn is_allowed_cloudflare_cookie_name(name: &str) -> bool { | "__cfruid" | "__cfseq" | "__cfwaitingroom" + | "__oailb" | "_cfuvid" | "cf_clearance" | "cf_ob_info" @@ -265,6 +268,37 @@ mod tests { ); } + #[test] + fn oailb_cookies_are_replayed_with_scope_and_expiration() { + let store = ChatGptCloudflareCookieStore::default(); + let url = reqwest::Url::parse("https://chatgpt.com/backend-api/codex/responses").unwrap(); + let cookie = HeaderValue::from_static( + "__oailb=route; Path=/backend-api; Max-Age=3600; Secure; HttpOnly; SameSite=Lax", + ); + store.set_cookies(&mut std::iter::once(&cookie), &url); + + let followup_url = reqwest::Url::parse("https://chatgpt.com/backend-api/ps/mcp").unwrap(); + assert_eq!( + store.cookies(&followup_url), + Some(HeaderValue::from_static("__oailb=route")) + ); + for outside_scope in [ + "https://chatgpt.com/", + "https://other.chatgpt.com/backend-api/ps/mcp", + "https://api.openai.com/backend-api/ps/mcp", + "http://chatgpt.com/backend-api/ps/mcp", + ] { + let outside_scope = reqwest::Url::parse(outside_scope).unwrap(); + assert_eq!(store.cookies(&outside_scope), None); + } + + let expired = HeaderValue::from_static( + "__oailb=; Path=/backend-api; Max-Age=0; Secure; HttpOnly; SameSite=Lax", + ); + store.set_cookies(&mut std::iter::once(&expired), &url); + assert_eq!(store.cookies(&followup_url), None); + } + #[test] fn ignores_non_chatgpt_cookies() { let store = ChatGptCloudflareCookieStore::default();