add credentials_mode for proper CORS credentials handling

This commit is contained in:
Muki Kiboigo committed 2026-09-04 06:59:39 -07:00
1 parent f31b32ac4e
commit ddfa034310
4 files changed
+75 -11

No files matched your search

+5
View File
@@ -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,
@@ -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,
+54 -6
View File
@@ -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;
}
}
}
+15 -5
View File
@@ -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 {