diff --git a/src/browser/tests/net/fetch.html b/src/browser/tests/net/fetch.html
index f96eb0640..a644937e0 100644
--- a/src/browser/tests/net/fetch.html
+++ b/src/browser/tests/net/fetch.html
@@ -518,3 +518,18 @@
});
}
+
+
diff --git a/src/browser/tests/net/headers.html b/src/browser/tests/net/headers.html
index 37279c349..e51f061e9 100644
--- a/src/browser/tests/net/headers.html
+++ b/src/browser/tests/net/headers.html
@@ -380,3 +380,47 @@
testing.expectEqual('b=2; HttpOnly', cookies[1]);
}
+
+
+
+
diff --git a/src/browser/tests/net/xhr.html b/src/browser/tests/net/xhr.html
index 929aff89f..9051f7a39 100644
--- a/src/browser/tests/net/xhr.html
+++ b/src/browser/tests/net/xhr.html
@@ -754,3 +754,19 @@ handler runs, abort() cancels the re-send and no load is delivered.
testing.expectEqual('9000!!!', first.over);
}
+
+
diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig
index d628e96e5..c94216599 100644
--- a/src/browser/webapi/net/Headers.zig
+++ b/src/browser/webapi/net/Headers.zig
@@ -12,6 +12,14 @@ const Execution = js.Execution;
const Headers = @This();
_list: KeyValueList,
+_guard: Guard = .none,
+
+// What mutation JS can make
+pub const Guard = enum {
+ none, // don't block anything
+ response, // block forbidden response headers
+ immutable, // block everythig
+};
pub const InitOpts = union(enum) {
obj: *Headers,
@@ -20,7 +28,11 @@ pub const InitOpts = union(enum) {
};
pub fn init(opts_: ?InitOpts, exec: *const Execution) !*Headers {
- const list = blk: {
+ return initGuarded(opts_, .none, exec);
+}
+
+pub fn initGuarded(opts_: ?InitOpts, guard: Guard, exec: *const Execution) !*Headers {
+ var list = blk: {
const opts = opts_ orelse break :blk KeyValueList.init();
switch (opts) {
.obj => |obj| break :blk try KeyValueList.copy(exec.arena, obj._list),
@@ -37,18 +49,56 @@ pub fn init(opts_: ?InitOpts, exec: *const Execution) !*Headers {
}
};
+ if (guard == .response) {
+ // easier to use the KVL's creation upfront and then strip these out
+ list.delete("set-cookie", null);
+ list.delete("set-cookie2", null);
+ }
+
return exec._factory.create(Headers{
._list = list,
+ ._guard = guard,
});
}
+pub fn isForbiddenResponseHeaderName(name: []const u8) bool {
+ if (std.ascii.eqlIgnoreCase(name, "set-cookie")) {
+ return true;
+ }
+
+ if (std.ascii.eqlIgnoreCase(name, "set-cookie2")) {
+ // yup, this is a real, never used / deprecated, header
+ return true;
+ }
+
+ return false;
+}
+
+const Mutation = enum { proceed, ignore };
+
+fn checkGuard(self: *const Headers, name: []const u8) !Mutation {
+ return switch (self._guard) {
+ .none => .proceed,
+ .immutable => error.TypeError,
+ .response => if (isForbiddenResponseHeaderName(name)) .ignore else .proceed,
+ };
+}
+
pub fn append(self: *Headers, name: []const u8, value: []const u8, exec: *const Execution) !void {
const normalized_name = normalizeHeaderName(name, exec.buf);
+ const mutation = try self.checkGuard(normalized_name);
+ if (mutation == .ignore) {
+ return;
+ }
try self._list.append(exec.arena, normalized_name, value);
}
-pub fn delete(self: *Headers, name: []const u8, exec: *const Execution) void {
+pub fn delete(self: *Headers, name: []const u8, exec: *const Execution) !void {
const normalized_name = normalizeHeaderName(name, exec.buf);
+ const mutation = try self.checkGuard(normalized_name);
+ if (mutation == .ignore) {
+ return;
+ }
self._list.delete(normalized_name, null);
}
@@ -76,6 +126,10 @@ pub fn has(self: *const Headers, name: []const u8, exec: *const Execution) bool
pub fn set(self: *Headers, name: []const u8, value: []const u8, exec: *const Execution) !void {
const normalized_name = normalizeHeaderName(name, exec.buf);
+ const mutation = try self.checkGuard(normalized_name);
+ if (mutation == .ignore) {
+ return;
+ }
try self._list.set(exec.arena, normalized_name, value);
}
diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig
index 79ff5f936..daf658eef 100644
--- a/src/browser/webapi/net/Response.zig
+++ b/src/browser/webapi/net/Response.zig
@@ -115,7 +115,7 @@ fn initWithArena(arena: *lp.Arena, body_: ?BodyInit, opts_: ?InitOpts, exec: *co
}
};
- const headers = try Headers.init(opts.headers, exec);
+ const headers = try Headers.initGuarded(opts.headers, .response, exec);
if (content_type) |ct| {
if (!headers.has("content-type", exec)) {
try headers.append("content-type", ct, exec);
@@ -151,7 +151,7 @@ pub fn createError(exec: *const Execution) !*Response {
._body = .empty,
._type = .@"error",
._is_redirected = false,
- ._headers = try Headers.init(null, exec),
+ ._headers = try .initGuarded(null, .immutable, exec),
};
arena.report();
return self;
@@ -171,7 +171,9 @@ pub fn createRedirect(url_: []const u8, status_: ?u16, exec: *const Execution) !
const location = try URL.resolve(arena.allocator(), exec.base(), url_, .{ .encoding = exec.charset.* });
const headers = try Headers.init(null, exec);
+ // append location directly, then lock the headers
try headers.set("location", location, exec);
+ headers._guard = .immutable;
const self = try arena.create(Response);
self.* = .{
@@ -204,7 +206,7 @@ pub fn createJson(data: js.Value, opts_: ?InitOpts, exec: *const Execution) !*Re
const opts = opts_ orelse InitOpts{};
const status_text = if (opts.statusText) |st| try arena.dupe(u8, st) else "";
- const headers = try Headers.init(opts.headers, exec);
+ const headers = try Headers.initGuarded(opts.headers, .response, exec);
if (!headers.has("content-type", exec)) {
try headers.append("content-type", "application/json", exec);
}
@@ -553,7 +555,7 @@ pub fn clone(self: *const Response, exec: *const Execution) !*Response {
._body = body,
._type = self._type,
._is_redirected = self._is_redirected,
- ._headers = try Headers.init(.{ .obj = self._headers }, exec),
+ ._headers = try .initGuarded(.{ .obj = self._headers }, self._headers._guard, exec),
._http_transfer = null,
};
arena.report();
diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig
index d503ac2bb..dc88a5e57 100644
--- a/src/browser/webapi/net/XMLHttpRequest.zig
+++ b/src/browser/webapi/net/XMLHttpRequest.zig
@@ -634,6 +634,9 @@ fn httpHeaderDoneCallback(transfer: *Transfer) !Transfer.HeaderResult {
var it = transfer.responseHeaderIterator();
while (it.next()) |hdr| {
+ if (Headers.isForbiddenResponseHeaderName(hdr.name)) {
+ continue;
+ }
const joined = try std.fmt.allocPrint(self._arena.allocator(), "{s}: {s}", .{ hdr.name, hdr.value });
try self._response_headers.append(self._arena.allocator(), joined);
}
diff --git a/src/testing.zig b/src/testing.zig
index c1706cb36..71976ac37 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -1140,6 +1140,15 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
});
}
+ if (std.mem.eql(u8, path, "/set_cookie")) {
+ return req.respond("", .{
+ .extra_headers = &.{
+ .{ .name = "Set-Cookie", .value = "lp_hidden=1; Path=/set_cookie_scope" },
+ .{ .name = "X-Visible", .value = "yes" },
+ },
+ });
+ }
+
if (std.mem.eql(u8, path, "/redirect_same_echo_headers")) {
// Same-origin 302 to /echo_headers: Authorization must survive the hop.
return req.respond("", .{