Bridge security
The bridge gives a page your app’s commands, Sabine’s window and guest controls, and the app’s events. Sabine decides per document whether a page gets it, and checks again on every call. There is no separate permission layer on top: anything a trusted page can call is something your app chose to expose, so the rules below decide who can reach your Rust code.
Which documents get the bridge
Section titled “Which documents get the bridge”A document gets the bridge when all of these hold:
- It is the main frame. Iframes never get the bridge, whatever their origin, including iframes of the app’s own pages.
- The app trusts its origin. That is one of:
- the app’s own files at
sabine://app/, when the window loads its local entry; - the origin of the window’s production
url; - in development, the origin of the development server, on
localhost,127.0.0.1and[::1]; - an origin you allowed with
allowed_originsor.allowed_origin(..).
- the app’s own files at
- Its security origin matches its URL. A document from an allowed URL that runs with an opaque
origin, such as one sandboxed by a
Content-Security-Policy: sandboxheader, does not inherit the URL’s trust.
file: URLs, data: URLs the app did not supply, about: pages and DevTools never get the
bridge. When a trusted page navigates to an origin the app doesn’t trust, the new document has no
window.sabine, and isAvailable() from @lantharos/sabine returns false.
The checks run in the renderer, before the bridge script is installed, and again in the browser process for every call and every event, against the document that is live at that moment. An answer is delivered only to the JavaScript context that made the call, so a navigation, a reload or a reused request id cannot redirect it into another document.
Allowing other origins
Section titled “Allowing other origins”An app whose page comes from the web, or navigates between its own sites, allows each origin in
Sabine.toml:
[web]url = "https://app.example.com"allowed_origins = ["https://accounts.example.com"]or in Rust:
window.allowed_origin("https://accounts.example.com")An origin is a scheme, host and optional port, with no path, query or fragment:
https://example.com or http://localhost:8080. Entries with a path are ignored. Only http and
https origins can be allowed. An allowed origin gets the whole bridge, the same as the app’s own
pages.
.url(..) allows its own URL’s origin, and a development run allows the development server’s
origin on every loopback name, so neither needs an allowed_origins entry. sabine dev hands the
development server’s URL to the app in SABINE_DEV_URL as it starts it. Builds made by
sabine bundle ignore that variable, so nobody can point an installed app at another page by
setting it.
Commands for one more origin
Section titled “Commands for one more origin”A command that one more site should reach, without that site getting the rest of the bridge, is
registered with a BridgeCommandDescriptor:
use sabine::prelude::*;use sabine::BridgeCommandDescriptor;
fn main() { SabineWindow::main(|window| { Ok(window.app().bridge_descriptor_handler( BridgeCommandDescriptor::new("checkout.complete") .allowed_origin("https://pay.example.com"), |command| { if command.origin.as_deref() == Some("https://pay.example.com") { record_payment(&command.params); } Ok(BridgeResponse::json(serde_json::Value::Null)) }, )) });}
fn record_payment(_receipt: &serde_json::Value) {}A page from that origin gets window.sabine and can call checkout.complete, and nothing else:
no other command, no window controls, no guests and no events. Its bridge.commands() lists only
checkout.complete, and calls to other commands reject right away. The app’s own pages and
allowed origins can call the command as usual.
Guests
Section titled “Guests”Guests, the other web pages a window shows beside its own, have no bridge by
default. A guest created with allowBridge: true gets it on the same terms as the window’s page:
only for documents from the app’s own pages, its allowed origins, or HTML the app passed in as
html. Such a guest can call the app’s commands and receives its events, but cannot create,
change or close guests or the popup; those calls fail with an error.
HTML that the app passes to guest.create or guest.navigate as html stays trusted for that
guest. Other data: URLs don’t gain access, even when the guest navigates to them itself.
The popup surface never has the bridge.
Handling what pages send
Section titled “Handling what pages send”Treat every call as input from the page, and the page as code that may one day show content you did not write: a message, a document, a remote resource. The bridge checks who calls; your handler decides what a call may do.
- Let serde check the shape. Typed commands reject params that don’t deserialize before your
handler runs. Use specific types, such as an enum for a choice and
u64for an id, instead of strings that you parse. - Bound sizes and counts. Params can be up to 1 MiB of JSON and bytes up to 32 MiB per call. Check lengths before you allocate or store, and reject what your app has no use for.
- Don’t take paths from the page. Resolve names or ids the page sends against a folder the
app owns, such as the app data folder, and
refuse names that contain separators or
... Let people pick files through the system’s file dialog when they choose them. - Never run what the page sends. Don’t pass page strings to a shell, to
Commandarguments that a program interprets, or to SQL without parameters. - Check
command.originwhen it matters. A raw handler sees the calling document’s origin, such assabine://apporhttps://app.example.com, andcommand.windownames the calling window. A command that one more origin may call can branch on it. - Treat URLs and documents from the desktop as input too. URLs and files the app receives from links and documents come from other programs. Validate their scheme and contents before acting on them.
Development builds
Section titled “Development builds”Development runs open Chromium’s remote debugging port on 127.0.0.1, 9222 unless
SABINE_DEVTOOLS_PORT names another, and add Open DevTools to the page’s context menu.
Production runs open neither unless the app calls .debug(port). Leave .debug out of builds
you ship: anything on the machine that can reach the port can drive the page.
A run is a development run when SABINE_ENV is development or SABINE_DEV_URL is set, which
sabine dev does. A build made for production, with SABINE_ENV=production while it compiles as
sabine bundle does, always runs in production: it ignores SABINE_ENV, SABINE_DEV_URL,
SABINE_DEVTOOLS_PORT, SABINE_APP_ID and SABINE_MANIFEST_PATH, and reads
its manifest only from its bundle.
The security model covers the rest of Sabine’s sandboxing: browser permission prompts, the Chromium sandbox and the app sandbox.