Skip to content

CORS Implementation - #3002

Merged
krichprollsch merged 41 commits into
mainfrom
cors-impl
Sep 4, 2026
Merged

krichprollsch merged 41 commits into
mainfrom
cors-impl

Conversation

@mookums

@mookums mookums commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

This adds a CORS implementation as experimental feature for now.
Enable it with --experimental-feature cors

fix #2015

@mookums
mookums force-pushed the cors-impl branch 2 times, most recently from ab8be3b to 235f9ea Compare July 24, 2026 14:01
@mookums

mookums commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Not ready yet, just want CI to start running on this branch.

@mookums
mookums marked this pull request as ready for review July 24, 2026 14:02

@karlseguin karlseguin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I ran the demo tests, and there wasn't 1 OPTIONS request.
  • The singleflight is nice, but it might have been a mistake to land it with this PR. It introduces bugs not related to CORS. I'd consider landing Singlelight first.
  • Without caching CORS responses, this seems like it could hurt performance more than initially anticipated.

Comment thread src/browser/URL.zig Outdated
return raw[0..authority_end];
}

pub fn isSameOrigin(url: [:0]const u8, origin: [:0]const u8) bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frame.isSameOrigin should use this (there's a comment there that assumes Protocols are equal, but I'm not sure why that's true).

Comment thread src/network/HttpClient.zig Outdated
owned.credentials = try arena.dupeZ(u8, c);
}

if (req.authored_headers.len > 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callsite dupes these in call_arena, and then we dupe it again in transfer.arena. We should rework headers before landing this.Request.headers is a micro-optimization which put the headers in a curl-friendly list from the get go. It was always questionable, and if we're going to iterate + dupe + iterate + dupe, I think it would both be cleaner and more efficient to:
1 - A single header list
2 - The header has a authored: bool flag
3 - The caller sets the headers after newRequest is called so that transfer.arena is available

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in #3118

I want to work on referrer policy, and I'll have the same issue.

Comment thread src/network/HttpClient.zig Outdated
.GET, .HEAD => false,
else => true,
};
if (!is_cross_origin and !is_unsafe_method) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's two different types of double negative going on here. They can all be eliminated.

const is_same_origin = URL.isSameOrigin(req.cookie_origin, req.url);
const is_safe_method = switch (req.method) {
    .GET, .HEAD => true,
    else => false,
};

if (is_same_origin and is_safe_method) {
    return;
}

Comment thread src/network/RobotsGate.zig Outdated
switch (res) {
.queued => {
// joined inflight fetch so release it.
client.arena_pool.release(arena);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a UAF bug in SingleFlight (1) related to this, BUT, as-is there's no reason to even acquire the arena in this case.

(1) Commented in SingleFlight.enter

Comment thread src/network/SingleFlight.zig
Comment thread src/network/CorsGate.zig Outdated
const lp = @import("lightpanda");

const http = @import("http.zig");
const Request = @import("../browser/webapi/net/Request.zig");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused

Comment thread src/network/CorsGate.zig Outdated
Comment thread src/network/CorsGate.zig Outdated
Comment thread src/network/CorsGate.zig Outdated
Comment thread src/network/RobotsGate.zig Outdated
pull Bot pushed a commit to chizee/browser that referenced this pull request Aug 3, 2026
Adding headers to an HTTP request was a bit awkward due to my desire to avoid
having an intermediate representation (e.g. an ArrayList(Header)). Going
straight to a curl slist avoids double-copying the headers (first to Zig, then
to curl).

But the CORS work (lightpanda-io#3002) showcases
that this micro-optimization simply isn't worth it, since it needs that
intermediate representation anyways.

And, this change isn't just for CORS. Headers have been a silly pain in the past
like unclear ownership, and messy APIs used in _a lot_ of places (WebBotAuth,
WebSocket, Fetch, ...)

This new approach stores headers on the transfer in an ArrayList. The API is:

```
const transfer = try client.newRequest(.{...}, owner);
{
    errdefer transfer.deinit();
    try transfer.addHeader("Over", "9000", .{});
}
try transfer.submit();
```

This:
1 - Eliminates ambiguity about errdefer cleanup responsibility
2 - Eliminates a bunch of stringZ concat that Frame, Config, CDP were doing
3 - Transfer.arena is now available for headers
@mookums mookums mentioned this pull request Aug 13, 2026
@mookums
mookums force-pushed the cors-impl branch 2 times, most recently from 5e1c6dd to 4eb88c0 Compare August 18, 2026 20:02
@mookums

mookums commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Redid a lot of the implementation of the CorsGate. I'm happier with this one. I purposefully omitted the CorsStore that I wrote, from my testing (mostly just running the WPT tests or the integration tests against it) i rarely if ever got a proper CORS cache hit with the store.

It is currently disabled by default behind an --obey-cors flag.

@krichprollsch

Copy link
Copy Markdown
Member

As far as I can see, chrome sends the pre-flight request each time... It doesn't use any cache.

@karlseguin

Copy link
Copy Markdown
Collaborator

As far as I can see, chrome sends the pre-flight request each time... It doesn't use any cache.

It does if the response includes a access-control-max-age header

@mookums mookums mentioned this pull request Aug 20, 2026
Comment thread src/help.zon Outdated
\\ --obey-robots
\\ Fetches and obeys robots.txt of the target page.
\\ Defaults to false.
\\ --obey-cors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be above --obey-robots

@karlseguin

Copy link
Copy Markdown
Collaborator

i started to review / test this, but I ran into a couple show-stoppers.

  1. It panics on a preflight deny (trivial to fix, but makes it impossible to test)
  2. It seems to block things it shouldn't? I expect to see 0 on virtually every website.
zig build run -- fetch --obey-cors "https://www.theverge.com/"  2>&1 | grep "CorsBlocked" | wc -l
24

Doesn't seem right.

@mookums

mookums commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

2 is fixed. It was blocking some scripts that are meant to be processed as no-cors by default, mostly analytics and tracking stuff.

w.r.t 1, I'm not sure how it would panic on a preflight deny? It just sets allowed as false and will resolve following the same path as preflight accepts and just resolve the requests as blocked.

@mookums

mookums commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

as a follow up, I ran it against the /cors/preflight-failure.htm WPT test that does preflight failures and I got no panics

@karlseguin

Copy link
Copy Markdown
Collaborator

use the wptrunner in the demo project to run /cors/. It'll crash.

@mookums

mookums commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

I always run it with --log-filter-scopes -all,+cors so never came up for me.

@karlseguin karlseguin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A redirect to a different origin doesn't trigger a CORS check. It might be worth looking at how redirects are handled holistically, or maybe there's a simple / clean fix for CORS.

But, for now, I'm less worried about places where CORS doesn't block where it should, but rather places where CORS blocks when it shouldn't (since that can break existing use-cases). There's ~4 different ways that can happen, all commented inline.

Comment thread src/network/CorsGate.zig Outdated
Comment thread src/network/HttpClient.zig Outdated
Comment thread src/network/HttpClient.zig Outdated
.url = "http://example.com/",
.cookie_jar = null,
.cookie_origin = "",
.origin = "",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit more correct to pass null for all these tests. I don't think you'll ever get a "" origin in the code.

Comment thread src/network/CorsGate.zig
Comment thread src/network/HttpClient.zig Outdated
try transfer.materializeResponse(msg.conn, .{});

// Validate the headers for the response with CORS.
if (transfer._cors_cross_origin) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some references online to how this behaves with a 304, but I couldn't find anything official in the spec. There's at least a few mentions that a 304 doesn't have to (or maybe shouldn't) include CORS headers and they should come from the cached value. /cors/304.htm is currently 0/4 which tells me we're doing it wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the WPT tests also seem to want it to resolve the CORS status from the HTTP cache.

Comment thread src/browser/webapi/net/Fetch.zig Outdated
Comment thread src/network/CorsGate.zig Outdated
Comment thread src/network/HttpClient.zig Outdated
@karlseguin

Copy link
Copy Markdown
Collaborator

No caching

Comment thread src/help.zon Outdated
@krichprollsch

krichprollsch commented Aug 28, 2026

Copy link
Copy Markdown
Member

Shouldn't HttpClient.Rquest.getCookieString check credentials_mode before writing cookies?

EDIT: it seems we also should check credentials_mode when we handle Set-Cookie from response (into materializeResponse)

Comment thread src/browser/webapi/net/Fetch.zig Outdated
Comment thread src/browser/webapi/WorkerGlobalScope.zig Outdated
@mookums
mookums force-pushed the cors-impl branch 2 times, most recently from 3275163 to 7dc3257 Compare August 31, 2026 14:15
@krichprollsch
krichprollsch merged commit 2f76e19 into main Sep 4, 2026
26 checks passed
@krichprollsch
krichprollsch deleted the cors-impl branch September 4, 2026 16:26
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement CORS mechanism

3 participants