From 58e35b0ce9e3de4bd5011b9c0477d9d3f7fbdca5 Mon Sep 17 00:00:00 2001 From: ManhND Date: Tue, 23 Jun 2026 23:33:43 +0000 Subject: [PATCH] fix: restrict CORS headers to localhost origins instead of wildcard Replace Access-Control-Allow-Origin: * with origin validation that only allows requests from local addresses (127.0.0.1, localhost, [::1]). The helper server already binds to 127.0.0.1 (loopback only), but the wildcard CORS header unnecessarily allows any web page to make cross-origin requests to it. This hardens the security posture by: - Adding cors_allowed_origin() to extract and validate the Origin header - Adding is_local_origin() to whitelist only loopback addresses - Adding cors_headers_for_origin() to conditionally emit CORS headers - Adding Vary: Origin header for correct caching behavior - Updating all 4 CORS locations: inline responses, write_http_response, write_http_stream_headers, and all proxy handler call sites Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- crates/codex-plus-core/src/launcher.rs | 65 ++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/codex-plus-core/src/launcher.rs b/crates/codex-plus-core/src/launcher.rs index 6eb9fcbbb..d885a1076 100644 --- a/crates/codex-plus-core/src/launcher.rs +++ b/crates/codex-plus-core/src/launcher.rs @@ -713,6 +713,7 @@ async fn handle_helper_connection( let path = raw_path.split('?').next().unwrap_or(raw_path); let request_body = http_request_body(&request); let request_user_agent = header_value_from_request(&request, "user-agent"); + let allowed_origin = cors_allowed_origin(&request); let remote_addr_text = remote_addr.map(|addr| addr.to_string()); let _ = crate::diagnostic_log::append_diagnostic_log( @@ -734,6 +735,7 @@ async fn handle_helper_connection( method, path, remote_addr_text, + &allowed_origin, ) .await; } @@ -745,6 +747,7 @@ async fn handle_helper_connection( method, path, remote_addr_text, + &allowed_origin, ) .await; } @@ -755,6 +758,7 @@ async fn handle_helper_connection( method, path, remote_addr_text, + &allowed_origin, ) .await; } @@ -837,13 +841,14 @@ async fn handle_helper_connection( "remote_addr": remote_addr_text }), ); + let cors = cors_headers_for_origin(&allowed_origin); let response = if method == "OPTIONS" { format!( - "HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type, Authorization\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + "HTTP/1.1 204 No Content\r\n{cors}Content-Length: 0\r\nConnection: close\r\n\r\n" ) } else { format!( - "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type, Authorization\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\n{cors}Content-Length: {}\r\nConnection: close\r\n\r\n", body.len() ) }; @@ -912,6 +917,7 @@ async fn handle_models_proxy_connection( method: &str, path: &str, remote_addr_text: Option, + allowed_origin: &str, ) -> anyhow::Result<()> { if method == "OPTIONS" { write_http_response( @@ -919,6 +925,7 @@ async fn handle_models_proxy_connection( "204 No Content", "application/json; charset=utf-8", &[], + allowed_origin, ) .await?; stream.shutdown().await?; @@ -938,6 +945,7 @@ async fn handle_models_proxy_connection( "502 Bad Gateway", "application/json; charset=utf-8", &body, + allowed_origin, ) .await?; log_helper_response( @@ -960,7 +968,7 @@ async fn handle_models_proxy_connection( upstream.content_type.clone() }; let body = upstream.response.bytes().await?.to_vec(); - write_http_response(stream, &status, &content_type, &body).await?; + write_http_response(stream, &status, &content_type, &body, allowed_origin).await?; log_helper_response( if is_success { "helper.models_proxy_ok" @@ -983,6 +991,7 @@ async fn handle_protocol_proxy_connection( method: &str, path: &str, remote_addr_text: Option, + allowed_origin: &str, ) -> anyhow::Result<()> { let request_json = serde_json::from_str::(request_body).ok(); let upstream = @@ -1000,6 +1009,7 @@ async fn handle_protocol_proxy_connection( "502 Bad Gateway", "application/json; charset=utf-8", &body, + allowed_origin, ) .await?; log_helper_response( @@ -1024,7 +1034,7 @@ async fn handle_protocol_proxy_connection( &upstream_body, ); let body = serde_json::to_vec(&error)?; - write_http_response(stream, &status, "application/json; charset=utf-8", &body).await?; + write_http_response(stream, &status, "application/json; charset=utf-8", &body, allowed_origin).await?; log_helper_response( "helper.protocol_proxy_upstream_error", method, @@ -1037,7 +1047,7 @@ async fn handle_protocol_proxy_connection( } if upstream.is_stream { - write_http_stream_headers(stream, "200 OK", "text/event-stream; charset=utf-8").await?; + write_http_stream_headers(stream, "200 OK", "text/event-stream; charset=utf-8", allowed_origin).await?; if upstream.wire_api == crate::protocol_proxy::UpstreamWireApi::Responses { let mut bytes_stream = upstream.response.bytes_stream(); while let Some(chunk) = bytes_stream.next().await { @@ -1115,6 +1125,7 @@ async fn handle_protocol_proxy_connection( &upstream.content_type }, &upstream_body, + allowed_origin, ) .await?; log_helper_response( @@ -1135,7 +1146,7 @@ async fn handle_protocol_proxy_connection( crate::protocol_proxy::chat_completion_to_response(chat_json)? }; let body = serde_json::to_vec(&response_json)?; - write_http_response(stream, "200 OK", "application/json; charset=utf-8", &body).await?; + write_http_response(stream, "200 OK", "application/json; charset=utf-8", &body, allowed_origin).await?; log_helper_response( "helper.protocol_proxy_ok", method, @@ -1154,6 +1165,7 @@ async fn handle_chat_completions_proxy_connection( method: &str, path: &str, remote_addr_text: Option, + allowed_origin: &str, ) -> anyhow::Result<()> { let upstream = match crate::protocol_proxy::open_chat_completions_proxy_request( request_body, @@ -1172,6 +1184,7 @@ async fn handle_chat_completions_proxy_connection( "502 Bad Gateway", "application/json; charset=utf-8", &body, + allowed_origin, ) .await?; log_helper_response( @@ -1195,7 +1208,7 @@ async fn handle_chat_completions_proxy_connection( }; if upstream.is_stream && is_success { - write_http_stream_headers(stream, &status, &content_type).await?; + write_http_stream_headers(stream, &status, &content_type, allowed_origin).await?; let mut bytes_stream = upstream.response.bytes_stream(); while let Some(chunk) = bytes_stream.next().await { stream.write_all(&chunk?).await?; @@ -1212,7 +1225,7 @@ async fn handle_chat_completions_proxy_connection( } let body = upstream.response.bytes().await?.to_vec(); - write_http_response(stream, &status, &content_type, &body).await?; + write_http_response(stream, &status, &content_type, &body, allowed_origin).await?; log_helper_response( if is_success { "helper.chat_completions_proxy_ok" @@ -1233,9 +1246,11 @@ async fn write_http_response( status: &str, content_type: &str, body: &[u8], + allowed_origin: &str, ) -> anyhow::Result<()> { + let cors = cors_headers_for_origin(allowed_origin); let response = format!( - "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type, Authorization\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\n{cors}Content-Length: {}\r\nConnection: close\r\n\r\n", body.len() ); stream.write_all(response.as_bytes()).await?; @@ -1247,9 +1262,11 @@ async fn write_http_stream_headers( stream: &mut tokio::net::TcpStream, status: &str, content_type: &str, + allowed_origin: &str, ) -> anyhow::Result<()> { + let cors = cors_headers_for_origin(allowed_origin); let response = format!( - "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nCache-Control: no-cache\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type, Authorization\r\nConnection: close\r\n\r\n" + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nCache-Control: no-cache\r\n{cors}Connection: close\r\n\r\n" ); stream.write_all(response.as_bytes()).await?; Ok(()) @@ -1394,6 +1411,34 @@ fn sanitize_diagnostic_event(event: &str) -> String { } } +fn cors_allowed_origin(request: &str) -> String { + match header_value_from_request(request, "origin") { + Some(origin) if is_local_origin(&origin) => origin, + None => "http://127.0.0.1".to_string(), + Some(_) => String::new(), + } +} + +fn is_local_origin(origin: &str) -> bool { + if origin == "null" { + return true; + } + let authority = match origin.find("://") { + Some(pos) => &origin[pos + 3..], + None => return false, + }; + let host = authority.split(':').next().unwrap_or(authority); + matches!(host, "127.0.0.1" | "localhost" | "[::1]" | "::1") +} + +fn cors_headers_for_origin(allowed_origin: &str) -> String { + if allowed_origin.is_empty() { + String::new() + } else { + format!("Access-Control-Allow-Origin: {allowed_origin}\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type, Authorization\r\nVary: Origin\r\n") + } +} + pub fn build_codex_arguments(debug_port: u16, extra_args: &[String]) -> Vec { let mut args = vec![ format!("--remote-debugging-port={debug_port}"),