Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/validation/worker-options-load-policy-701-81-227.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ measurement was executed for this change. Those gates remain acceptance work
under #81 and #227. MessagePort reachability remains owned by #288, and general
nested-document navigation and security remain owned by #267.

The host resource API exposes the final response rather than each intermediate
redirect response. It receives the worker fetch mode, origin, and credentials
and therefore remains responsible for enforcing every redirect hop; the worker
runtime independently validates the returned final URL and response. A future
resource-contract revision would be needed for engine-side, per-hop CORS audit.
Follow-up #704 extends the admitted-resource response with a bounded redirect
ledger. Worker loads now require that ledger when the final URL differs and
validate each hop before publishing the script. The provider remains
responsible for performing transport and reporting its credential-forwarding
decision truthfully.
56 changes: 56 additions & 0 deletions docs/validation/worker-redirect-hop-policy-704-81-227.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Worker redirect-hop policy (#704, #81, #227)

## Admitted-resource contract

The product-neutral resource response can now carry an ordered redirect ledger.
Each entry contains only the source URL, destination URL, redirect status,
credential-forwarding decision, admitted CORS origin, and CORS-credentials
decision. It carries no cookies, authorization values, bodies, or general
response headers.

The public `webscene_resource_response_v4` adds the ledger as an optional tail.
Existing callbacks remain valid because the engine zero-initializes that tail;
providers compiled against the extended declaration check `struct_size` before
writing it. The native adapter accepts at most 20 hops, 8 KiB per URL/origin
field, and 64 KiB for all response headers and redirect metadata combined.
Unknown flags, unsupported redirect statuses, invalid pointers, control bytes,
and oversized metadata fail the resource load.

## Worker enforcement

A Worker response whose final URL differs from its requested URL must provide a
complete ledger. The first source must equal the request URL, every destination
must equal the next source, and the final destination must equal the response's
final URL. Only HTTP(S) URLs and 301, 302, 303, 307, and 308 statuses are
accepted. Repeated destinations, more than 20 hops, malformed URLs, and HTTPS
to HTTP downgrades fail closed.

Classic worker loads continue to require the creator origin at every source and
destination. Module CORS loads validate each cross-origin redirect response's
admitted origin and require its credentials decision for `include`. A wildcard
origin is rejected for `include`. A provider may not report forwarded
credentials for `omit`, or for a cross-origin destination under `same-origin`.
The existing final-response status, origin, and CORS checks still run after the
ledger check.

The redirect ledger is validated after the asynchronous host callback and
before response cookies, source bytes, the final dependency base, compilation,
or an error can be published. Worker generation checks on both sides of the
callback continue to suppress late completion after termination, navigation,
or engine teardown. Terminal failure remains exactly once through #701's
bounded error and slot-retirement path.

All existing limits remain unchanged: 64 live Workers, 256 retained stopped
wrappers, 256 messages and 16 MiB per message direction, and 16 bounded error
strings. Redirect metadata adds fixed count and byte caps without adding a
queue or retained background task.

## Deferred acceptance

No build, native suite, redirect/CORS WPT, failure-injection run, credential
review, lifecycle stress, performance, memory, package, or cross-platform CI
was executed under the implementation-first policy. Those gates remain under
#81 and #227. The provider still owns transport and must truthfully describe
each followed hop; a future callback-style handshake would be required for the
engine to authorize a destination before the provider performs network I/O.
MessagePort reachability #288 and general iframe lifecycle #267 are unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -1299,10 +1299,33 @@ typedef struct webscene_resource_header_v4 {
size_t value_length;
} webscene_resource_header_v4;

enum {
WEBSCENE_RESOURCE_REDIRECT_CREDENTIALS_FORWARDED_V4 = 1U << 0U,
WEBSCENE_RESOURCE_REDIRECT_CORS_CREDENTIALS_V4 = 1U << 1U
};

/* One followed network redirect. Buffers are borrowed for the callback only.
* CREDENTIALS_FORWARDED describes the request to destination_url.
* cors_allow_origin and CORS_CREDENTIALS describe the response received from
* source_url. Response headers and credentials are not copied into the
* redirect ledger. */
typedef struct webscene_resource_redirect_hop_v4 {
uint32_t struct_size;
uint32_t status;
uint32_t flags;
const char* source_url;
size_t source_url_length;
const char* destination_url;
size_t destination_url_length;
const char* cors_allow_origin;
size_t cors_allow_origin_length;
} webscene_resource_redirect_hop_v4;

/* Response metadata is borrowed only for the callback invocation. The engine
* copies accepted fields before returning to the host. Header count and total
* bytes are bounded; Set-Cookie is consumed by the cookie jar and is never
* exposed through the JavaScript Headers object. */
* copies accepted fields before returning to the host. Header, redirect-hop,
* and total metadata counts are bounded; Set-Cookie is consumed by the cookie
* jar and is never exposed through the JavaScript Headers object. A callback
* must check struct_size before writing the optional redirect_hops tail. */
typedef struct webscene_resource_response_v4 {
uint32_t struct_size;
uint32_t status;
Expand All @@ -1312,6 +1335,8 @@ typedef struct webscene_resource_response_v4 {
size_t final_url_length;
const webscene_resource_header_v4* headers;
size_t header_count;
const webscene_resource_redirect_hop_v4* redirect_hops;
size_t redirect_hop_count;
} webscene_resource_response_v4;

typedef struct webscene_resource_request_context_v4 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,21 @@
constexpr size_t maximum_response_headers = 64U;
constexpr size_t maximum_response_metadata_bytes = 64U * 1024U;
constexpr size_t maximum_response_url_bytes = 8U * 1024U;
constexpr size_t maximum_response_redirect_hops = 20U;
if (native_response.status < 100U || native_response.status > 599U
|| native_response.status_text_length > 1024U
|| native_response.final_url_length > maximum_response_url_bytes
|| native_response.header_count > maximum_response_headers
|| native_response.redirect_hop_count
> maximum_response_redirect_hops
|| (native_response.status_text_length != 0U
&& native_response.status_text == nullptr)
|| (native_response.final_url_length != 0U
&& native_response.final_url == nullptr)
|| (native_response.header_count != 0U
&& native_response.headers == nullptr)) {
&& native_response.headers == nullptr)
|| (native_response.redirect_hop_count != 0U
&& native_response.redirect_hops == nullptr)) {
return failed("invalid-response-metadata");
}
response.status = native_response.status;
Expand Down Expand Up @@ -294,6 +299,73 @@
response.cacheable = false;
}
}
response.redirect_hops.reserve(
native_response.redirect_hop_count);
constexpr uint32_t known_redirect_flags =
WEBSCENE_RESOURCE_REDIRECT_CREDENTIALS_FORWARDED_V4
| WEBSCENE_RESOURCE_REDIRECT_CORS_CREDENTIALS_V4;
for (size_t index = 0;
index < native_response.redirect_hop_count;
++index) {
const auto& hop = native_response.redirect_hops[index];
if (hop.struct_size
< sizeof(webscene_resource_redirect_hop_v4)
|| (hop.status != 301U && hop.status != 302U
&& hop.status != 303U && hop.status != 307U
&& hop.status != 308U)
|| (hop.flags & ~known_redirect_flags) != 0U
|| hop.source_url == nullptr
|| hop.source_url_length == 0U
|| hop.source_url_length
> maximum_response_url_bytes
|| hop.destination_url == nullptr
|| hop.destination_url_length == 0U
|| hop.destination_url_length
> maximum_response_url_bytes
|| hop.cors_allow_origin_length
> maximum_response_url_bytes
|| (hop.cors_allow_origin_length != 0U
&& hop.cors_allow_origin == nullptr)) {
return failed("invalid-redirect-metadata");
}
metadata_bytes += hop.source_url_length
+ hop.destination_url_length
+ hop.cors_allow_origin_length;
if (metadata_bytes > maximum_response_metadata_bytes) {
return failed("response-metadata-too-large");
}
webscene_native::v8_dom_runtime::resource_redirect_hop copied;
copied.source_url.assign(
hop.source_url, hop.source_url_length);
copied.destination_url.assign(
hop.destination_url, hop.destination_url_length);
copied.cors_allow_origin.assign(
hop.cors_allow_origin == nullptr
? ""
: hop.cors_allow_origin,
hop.cors_allow_origin_length);
const auto invalid_url = [](const std::string& value) {
return value.find_first_of(" \t\r\n")
!= std::string::npos
|| value.find('\0') != std::string::npos;
};
if (invalid_url(copied.source_url)
|| invalid_url(copied.destination_url)
|| copied.cors_allow_origin.find_first_of("\r\n")
!= std::string::npos
|| copied.cors_allow_origin.find('\0')
!= std::string::npos) {
return failed("invalid-redirect-metadata");
}
copied.status = hop.status;
copied.credentials_forwarded = (hop.flags
& WEBSCENE_RESOURCE_REDIRECT_CREDENTIALS_FORWARDED_V4)
!= 0U;
copied.cors_credentials_allowed = (hop.flags
& WEBSCENE_RESOURCE_REDIRECT_CORS_CREDENTIALS_V4)
!= 0U;
response.redirect_hops.push_back(std::move(copied));
}
} else {
response.final_url = url;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,15 @@ class v8_dom_runtime final {
uint32_t preferred_color_scheme{WEBSCENE_PREFERRED_COLOR_SCHEME_LIGHT};
};

struct resource_redirect_hop final {
std::string source_url;
std::string destination_url;
std::string cors_allow_origin;
uint32_t status{};
bool credentials_forwarded{};
bool cors_credentials_allowed{};
};

struct resource_response final {
std::string content;
std::string entity_tag;
Expand All @@ -326,6 +335,7 @@ class v8_dom_runtime final {
std::string status_text{"OK"};
std::string final_url;
std::vector<std::pair<std::string, std::string>> headers;
std::vector<resource_redirect_hop> redirect_hops;
bool has_http_metadata{false};
};

Expand Down
Loading
Loading