From ddfa034310d250d162a3f5103cc682a25cc0f64d Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 18 Aug 2026 14:43:03 -0700 Subject: [PATCH] add credentials_mode for proper CORS credentials handling --- src/browser/webapi/net/Fetch.zig | 5 ++ src/browser/webapi/net/XMLHttpRequest.zig | 1 + src/network/CorsGate.zig | 60 ++++++++++++++++++++--- src/network/HttpClient.zig | 20 ++++++-- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 36a12a62c..c198d68ac 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -98,6 +98,11 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis .include => true, .@"same-origin" => exec.isSameOrigin(request._url), }, + .credentials_mode = switch (request._credentials) { + .omit => .omit, + .@"same-origin" => .same_origin, + .include => .include, + }, .origin = exec.origin(), .redirect = switch (request._redirect) { .follow => .follow, diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 21b7f825d..df65cf72e 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -316,6 +316,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v .method = self._method, .body = self._request_body, .cookies = cookie_support, + .credentials_mode = if (self._with_credentials) .include else .same_origin, .origin = exec.origin(), .resource_type = .xhr, .timeout_ms = self._timeout, diff --git a/src/network/CorsGate.zig b/src/network/CorsGate.zig index 39bf55b58..38fab7f57 100644 --- a/src/network/CorsGate.zig +++ b/src/network/CorsGate.zig @@ -44,7 +44,7 @@ const ACCESS_CONTROL_REQUEST_HEADERS = "access-control-request-headers"; const ACCESS_CONTROL_ALLOW_ORIGIN = "access-control-allow-origin"; const ACCESS_CONTROL_ALLOW_METHODS = "access-control-allow-methods"; const ACCESS_CONTROL_ALLOW_HEADERS = "access-control-allow-headers"; -const ACCESS_CONTROL_MAX_AGE = "access-control-max-age"; +const ACCESS_CONTROL_ALLOW_CREDENTIALS = "access-control-allow-credentials"; pub fn deinit(self: *CorsGate) void { self.single_flight.deinit(); @@ -154,6 +154,7 @@ const CorsPreflightContext = struct { origin: []const u8, method: http.Method, request_headers: []const []const u8, + wants_credentials: bool, allowed: bool = false, @@ -162,6 +163,7 @@ const CorsPreflightContext = struct { acao: ?[]const u8, acam: ?[]const u8, acah: ?[]const u8, + acac: ?[]const u8, ) bool { // Access-Control-Allow-Origin const allow_origin = acao orelse { @@ -169,7 +171,14 @@ const CorsPreflightContext = struct { return false; }; - if (!std.mem.eql(u8, allow_origin, "*") and !std.mem.eql(u8, allow_origin, self.origin)) { + const is_wildcard_origin = std.mem.eql(u8, allow_origin, "*"); + + if (is_wildcard_origin and self.wants_credentials) { + log.debug(.cors, "preflight blocked", .{ .url = self.url, .reason = "wildcard origin with credentials" }); + return false; + } + + if (!is_wildcard_origin and !std.mem.eql(u8, allow_origin, self.origin)) { log.debug(.cors, "preflight blocked", .{ .url = self.url, .reason = "origin mismatch", @@ -179,13 +188,27 @@ const CorsPreflightContext = struct { return false; } + // Access-Control-Allow-Credentials + if (self.wants_credentials) { + const allow_credentials = acac orelse { + log.debug(.cors, "preflight blocked", .{ .url = self.url, .reason = "missing acac" }); + return false; + }; + + if (!std.mem.eql(u8, allow_credentials, "true")) { + log.debug(.cors, "preflight blocked", .{ .url = self.url, .reason = "credentials not allowed", .allow_credentials = acac }); + return false; + } + } + // Access-Control-Allow-Methods const allow_methods = acam orelse { log.debug(.cors, "preflight blocked", .{ .url = self.url, .reason = "missing acam" }); return false; }; - if (!std.mem.eql(u8, allow_methods, "*") and !methodAllowed(allow_methods, self.method)) { + const methods_wildcard = std.mem.eql(u8, allow_methods, "*") and !self.wants_credentials; + if (!methods_wildcard and !methodAllowed(allow_methods, self.method)) { log.debug(.cors, "preflight blocked", .{ .url = self.url, .reason = "method not allowed", @@ -202,7 +225,8 @@ const CorsPreflightContext = struct { return false; }; - if (!std.mem.eql(u8, allow_headers, "*")) { + const headers_wildcard = std.mem.eql(u8, allow_headers, "*") and !self.wants_credentials; + if (!headers_wildcard) { for (self.request_headers) |name| { if (!headerAllowed(allow_headers, name)) { log.debug(.cors, "preflight blocked", .{ @@ -257,6 +281,7 @@ const CorsPreflightContext = struct { var acao: ?[]const u8 = null; var acam: ?[]const u8 = null; var acah: ?[]const u8 = null; + var acac: ?[]const u8 = null; var iter = transfer.responseHeaderIterator(); while (iter.next()) |hdr| { @@ -266,10 +291,12 @@ const CorsPreflightContext = struct { acam = hdr.value; } else if (std.ascii.eqlIgnoreCase(ACCESS_CONTROL_ALLOW_HEADERS, hdr.name)) { acah = hdr.value; + } else if (std.ascii.eqlIgnoreCase(ACCESS_CONTROL_ALLOW_CREDENTIALS, hdr.name)) { + acac = hdr.value; } } - self.allowed = self.validateHeaders(acao, acam, acah); + self.allowed = self.validateHeaders(acao, acam, acah, acac); return .proceed; } @@ -337,6 +364,7 @@ fn fetchThenResume(self: *CorsGate, transfer: *Transfer) !void { .origin = try arena.dupe(u8, transfer.req.origin orelse "null"), .method = transfer.req.method, .request_headers = header_names.items, + .wants_credentials = transfer.req.credentials_mode == .include, }; const fetch_transfer = try client.newRequest(.{ @@ -393,7 +421,15 @@ pub fn validateResponse(transfer: *Transfer) !void { return error.CorsBlocked; }; - if (!std.mem.eql(u8, allow_origin, "*")) { + const wants_credentials = req.credentials_mode == .include; + const is_wildcard_origin = std.mem.eql(u8, allow_origin, "*"); + + if (is_wildcard_origin and wants_credentials) { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "wildcard origin with credentials" }); + return error.CorsBlocked; + } + + if (!is_wildcard_origin) { const origin = req.origin orelse { log.warn(.cors, "blocked", .{ .url = req.url, .reason = "opaque origin" }); return error.CorsBlocked; @@ -409,4 +445,16 @@ pub fn validateResponse(transfer: *Transfer) !void { return error.CorsBlocked; } } + + if (wants_credentials) { + const allow_creds = HttpClient.findHeader(transfer.res.headers, ACCESS_CONTROL_ALLOW_CREDENTIALS) orelse { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "missing acac" }); + return error.CorsBlocked; + }; + + if (!std.mem.eql(u8, allow_creds, "true")) { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "credentials not allowed", .allow_credentials = allow_creds }); + return error.CorsBlocked; + } + } } diff --git a/src/network/HttpClient.zig b/src/network/HttpClient.zig index cbfcdc529..8a2fc2eab 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -634,8 +634,8 @@ pub fn newRequest(self: *Client, req: Request, owner: ?*Owner) anyerror!*Transfe }; owned.cookie_origin = null; - if (req.credentials) |c| { - owned.credentials = try arena.dupeZ(u8, c); + if (req.basic_auth_credentials) |c| { + owned.basic_auth_credentials = try arena.dupeZ(u8, c); } const raw_origin: ?[]const u8 = req.origin orelse if (owner) |o| o.origin.* else null; @@ -1808,13 +1808,23 @@ pub const Request = struct { // ten segments, versus a TCP handshake plus a TLS one. const HEADERS_ONLY_DRAIN_MAX: usize = 16 * 1024; + pub const CredentialsMode = enum { + // Never send credentials, even same-origin. + omit, + // Send credentials only for same-origin requests. + same_origin, + // Always send credentials, including cross-origin. + include, + }; + method: Method, url: [:0]const u8, body: ?[]const u8 = null, resource_type: ResourceType, redirect: RedirectMode = .follow, referrer_policy: ?referrer.Policy = null, - credentials: ?[:0]const u8 = null, + basic_auth_credentials: ?[:0]const u8 = null, + credentials_mode: CredentialsMode = .same_origin, timeout_ms: u32 = 0, skip_cache: bool = false, @@ -3097,7 +3107,7 @@ pub const Transfer = struct { } // add credentials - if (req.credentials) |creds| { + if (req.basic_auth_credentials) |creds| { if (self._auth_challenge != null and self._auth_challenge.?.source == .proxy) { try conn.setProxyCredentials(creds); } else { @@ -3282,7 +3292,7 @@ pub const Transfer = struct { } pub fn updateCredentials(self: *Transfer, userpwd: [:0]const u8) void { - self.req.credentials = userpwd; + self.req.basic_auth_credentials = userpwd; } pub const RequestHeader = struct {