Skip to content

Security

Checked against 2994ef5 on 2026-11-10 by Capy (review pass: Subagent 124 and fable). The files it covers are unchanged since.

The banner counts WPT’s cors/ directory. Most of its tests are older .htm pages whose shared cors/support.js relies on a server template niso’s test runner doesn’t fill in, so they stop with a syntax error before testing anything and count as failures that say nothing about CORS. Of the failures that do test CORS, here and in fetch/api/cors/, two fail open, allowing more than a browser would:

  • A malformed Access-Control-Expose-Headers (bb-8, no no) should expose no header. niso drops the invalid entries and exposes the valid ones the server named (list in crates/niso-core/src/net/fetch.rs; cors/access-control-expose-headers-parsing.window.js).
  • A cached preflight outlives a network error. When a request fails with a network error, a browser drops the preflight it cached for it; niso keeps it until it expires, so the next request goes out without a new preflight (fetch/api/cors/cors-preflight-cache.any.js).

The others found fail closed: a credentialed request carries no cookie, because niso has none (cors-cookies.any.js), and a header value with a control character in it is refused by the HTTP client, so the request fails rather than going out without a preflight (cors-preflight-not-cors-safelisted.any.js). Not examined: cors/preflight-cache-partitioning.sub.window.js, which fails with TypeError: not a function, and the tests that time out (cors/image-tainting-in-cross-origin-iframe.sub.html, fetch/api/cors/cors-keepalive.any.js). Each result is in tests/wpt/expectations.json.

A niso page is part of its app. It isn’t sandboxed from the app the way a WebView’s content is: native modules, the host’s loader and storage are the app’s own. So the security questions that matter are about what a page can learn from other origins, and niso answers those the way browsers do.

  • CORS: every cross-origin read is checked. A response from another origin that doesn’t pass CORS is opaque: a no-cors fetch gets an opaque response with no body, headers or status, and an <img> or <script> can use it without the page reading it. The same rule decides the rest: a canvas that draws an opaque image can’t be read back, a classic script from another origin reports only “Script error.”, cross-origin module scripts and fonts need CORS, and another origin’s style sheet rules can’t be read.
  • Failures say nothing: every failed fetch rejects with TypeError("Failed to fetch"), whatever the reason, so a page can’t scan ports or read headers through error messages.
  • Mixed content: a page delivered over HTTPS can’t make plain HTTP requests (images are upgraded), except to the machine itself.
  • Request rules: forbidden methods and headers (Cookie, Origin, Referer, Host, …) are refused in Rust, for every caller including C.
  • No credentials: no cookies, HTTP authentication or client certificates. A page can’t read a response tailored to a user’s session, because there’s no session; it can still reach whatever the device’s network reaches, including the LAN.
  • No Content Security Policy, CORP, COEP or COOP.
  • No isolation between origins in the process: every origin’s scripts share one process. The guarantees hold against script using web APIs, not against an exploit of the JS engine or a Spectre-style side channel.
  • No Private Network Access checks and no DNS rebinding protection.

Load only code you trust into a niso app, as you would into any native app. The threat model, with every rule and where it’s enforced, is Cross-origin reads.