From 4e79f2481475ed4d3f25ac1445f93b2bb3af705f Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 17 Aug 2026 09:19:31 -0700 Subject: [PATCH] basic validation without preflighting --- src/network/CorsGate.zig | 56 ++++++++++++++++++++++++++++++++++++-- src/network/HttpClient.zig | 12 +++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/network/CorsGate.zig b/src/network/CorsGate.zig index ea4753f31..eb20d5dda 100644 --- a/src/network/CorsGate.zig +++ b/src/network/CorsGate.zig @@ -26,6 +26,7 @@ const http = @import("http.zig"); const Network = @import("Network.zig"); const Transfer = @import("HttpClient.zig").Transfer; const SingleFlight = @import("SingleFlight.zig"); +const HttpClient = @import("HttpClient.zig"); const log = lp.log; const Allocator = std.mem.Allocator; @@ -103,7 +104,6 @@ fn requiresPreflight(transfer: *const Transfer) bool { const Result = enum { allowed, blocked, pending }; pub fn check(self: *CorsGate, transfer: *Transfer) !Result { - _ = self; const req = &transfer.req; if (req.origin) |origin| { @@ -113,8 +113,60 @@ pub fn check(self: *CorsGate, transfer: *Transfer) !Result { } } - log.debug(.cors, "cross origin", .{ .url = req.url, .origin = req.origin orelse "null" }); transfer._cors_cross_origin = true; + if (!requiresPreflight(transfer)) { + log.debug(.cors, "cross origin", .{ + .url = req.url, + .origin = req.origin orelse "null", + .preflight = false, + }); + return .allowed; + } + + log.debug(.cors, "cross origin", .{ + .url = req.url, + .origin = req.origin orelse "null", + .preflight = true, + }); + + _ = self; return .blocked; + + // try self.fetchThenResumse(transfer); + // return .pendind; +} + +pub fn validateResponse(transfer: *Transfer) !void { + const req = &transfer.req; + const allow_origin = HttpClient.findHeader(transfer.res.headers, "access-control-allow-origin"); + + if (allow_origin == null) { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "missing acao" }); + return error.CorsBlocked; + } + + const wants_credentials = req.credentials != null or transfer.findRequestHeader("Cookie") != null; + + if (!std.mem.eql(u8, allow_origin.?, "*")) { + const origin = req.origin orelse { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "opaque origin" }); + return error.CorsBlocked; + }; + if (!std.mem.eql(u8, allow_origin.?, origin)) { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "origin mismatch", .allow_origin = allow_origin.?, .origin = origin }); + return error.CorsBlocked; + } + } else if (wants_credentials) { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "wildcard with credentials" }); + return error.CorsBlocked; + } + + if (wants_credentials) { + const allow_creds = HttpClient.findHeader(transfer.res.headers, "access-control-allow-credentials"); + if (allow_creds == null or !std.mem.eql(u8, allow_creds.?, "true")) { + log.warn(.cors, "blocked", .{ .url = req.url, .reason = "credentials not allowed" }); + return error.CorsBlocked; + } + } } diff --git a/src/network/HttpClient.zig b/src/network/HttpClient.zig index 9a3c8c400..891c5e9dc 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -1085,7 +1085,7 @@ pub fn resumeAfterCors(self: *Client, transfer: *Transfer) !void { return self.pipeline(transfer, .after_cors); } -fn findHeader(headers: []const http.Header, name: []const u8) ?[]const u8 { +pub fn findHeader(headers: []const http.Header, name: []const u8) ?[]const u8 { for (headers) |hdr| { if (std.ascii.eqlIgnoreCase(hdr.name, name)) { return hdr.value; @@ -1690,6 +1690,16 @@ fn processOneMessage(self: *Client, msg: http.Handles.MultiMessage, transfer: *T try transfer.materializeResponse(msg.conn, .{}); + // Validate the headers for the response with CORS. + if (transfer._cors_cross_origin) { + CorsGate.validateResponse(transfer) catch |err| { + self.removeConn(msg.conn); + transfer._conn = null; + transfer.failAsync(err); + return true; + }; + } + // Latency is only meaningful for responses that hit the network (cache // and synthetic responses never reach processOneMessage). if (!transfer.req.internal) {