Browse API 1 documentation
GroveAPI 1preview

Security model

Native capability gates, origin isolation, and egress controls.

Mango 3.1+View Markdown

Grove treats extension code and every value it processes as untrusted. The sandbox is enforced by Mango and WebKit, not by JavaScript conventions inside the extension.

Local, isolated code

Mango installs validated package bytes and serves them from a per-extension grove-ext:// origin. It does not execute native code and does not remote-load the extension’s entry page. Package paths and resolved symlinks must stay inside the installed directory.

Separate origins and WebKit data stores isolate extensions from each other and from Mango’s own web content.

Native bridge authority

The injected facade is frozen as defense in depth, but native checks are the actual boundary. Mango validates:

  • which capability the user granted;
  • which channel, query, application, or server scope owns the instance;
  • targets, identifiers, payload shape, text length, and collection bounds;
  • whether a contributed native integration was declared in the manifest;
  • whether a call is valid for the current surface.

Application-scoped Roots are mirrored only to route passive app-wide contributions. Native gates exclude them from IRC messages, commands, filters, presence, and sends in every mirrored session, regardless of the capabilities a malicious manifest requests.

There is no raw IRC send API. Modeled sends flow through Mango’s IRC engine and rate limits.

Network egress

Every extension starts with network access blocked. Mango derives a WebKit content rule list and Content Security Policy from hostPermissions. Only declared https:// and wss:// hosts become reachable, and those schemes are granted independently.

The host also blocks or constrains common escape paths:

  • cleartext HTTP and WebSocket destinations;
  • top-level navigation away from the packaged entry document;
  • frames, objects, forms, and arbitrary child content;
  • window.open and link-preview loading;
  • WebRTC-based STUN or TURN egress;
  • undeclared image, media, beacon, fetch, and WebSocket destinations.

Use an exact origin when the package knows its service. A whole leftmost-label pattern such as https://*.example.com is available only for services that genuinely assign direct user subdomains; it does not match the apex or deeper names. Treat https://* or wss://* as a loud exception for a genuinely user-selected destination, not as a convenience during development.

Root-opened web panes

A server-scoped headless Root with ui.web-pane may ask Mango to open a packaged HTML page beside one explicit confirmed joined channel. Extensions should invoke it from a host-attributed native workflow such as a member action. Mango checks the package-local destination at the Root reducer, final IRC-session boundary, pane reducer, and WebView host. The page uses an ephemeral WebKit data store with no Grove facade, IRC events, native bridge, or access to the Root’s storage.

The pane has the same executable-code and network-egress restrictions as its owner. Top-level navigation stays in that package’s HTML pages. A user-activated external HTTP or HTTPS link leaves the pane through Mango’s ordinary link-opening behavior. The pane is transient authority: it is not restored after reconnect and closes automatically when its owning Root is stopped, replaced, or removed.

Packaged executable code only

Host permissions authorize network data, not executable code. Sprigs (including settings/composer pages and Root-owned panes) run JavaScript from their installed package origin only. Bundle dependencies and use external package-local script files, including ES modules. Inline scripts/HTML event handlers, eval, function constructors, string timers, remote/blob/data scripts, and workers are blocked. Runtime WebAssembly compilation is not enabled. Frames/iframes remain blocked, including package-local frames; compose the UI in one document. Move inline handlers to addEventListener in a packaged file; use precompiled templates and production bundles that do not require eval.

CSS inline styles remain allowed; this does not permit JavaScript. JSON, images, and other non-code resources can still be loaded under the existing data-origin permissions. Do not render untrusted API HTML or interpret API data as commands.

These controls do not prove a package is benign: a malicious package can implement an interpreter or follow remote instructions. Native capability checks, review, and origin restrictions remain necessary. User-opened isolated Web tabs keep normal website behavior and have no Grove bridge.

OAuth

OAuth authorization is a native capability, not permission to navigate the Sprig WebView. Mango opens ASWebAuthenticationSession, validates the initial HTTPS origin against hostPermissions, and accepts only the fixed mango://grove/oauth callback. For a direct flow the host replaces extension-supplied callback, response type, state, and PKCE parameters with random host-owned values. For a brokered flow grove.oauth.prepare() exposes only the callback, state, and S256 challenge needed to ask the service for its authorization URL; Mango keeps the verifier private until the callback validates. Only one authorization session may be active across the app.

See OAuth integrations for the complete direct and brokered implementation flows.

The provider’s visible authorization redirects can cross origins inside the system browser. That does not widen the extension’s WebView egress: token and user-info requests still need their exact HTTPS origins in hostPermissions. Mango returns the one-time code and verifier only after callback and state validation; it never receives or stores the resulting service tokens.

Use Authorization Code with S256 PKCE and a public client. Do not embed a client secret, place tokens in query strings, log authorization results, or reconstruct the validated callback from individual parameters. Persist tokens only with storage.secure, disable token-request redirects, bound provider responses, and remove obsolete grants when the user disconnects the service.

Native network requests

network.request deliberately bypasses browser CORS, so it is separate from both hostPermissions and ordinary WebView egress. All three gates must agree: the manifest declares the capability and destination, the user accepts them, and Mango validates every call. Native requests are HTTPS-only, DNS-pinned, cookie-free, size/concurrency/time bounded, and restricted to a small method and header allow-list. Mango returns a redirect response and its bounded Location header but never follows it automatically. An extension must validate the new destination and issue a separate request, which repeats host-permission, DNS, and private-address checks.

This bridge does not make a remote API trustworthy. Treat every status, header, JSON field, binary body, OAuth URL, asset identifier, and share URL as attacker-controlled input. Never render response HTML, forward an arbitrary server-provided credential header, or disclose a token in diagnostics. Prefer exact origins. https://* is appropriate only for an inherently user-selected self-hosted service and must remain a conspicuous consent item.

Render IRC data as data

An allow-list limits exfiltration, but it does not make injected markup safe. Nicknames, account names, topics, messages, URLs, command arguments, and backend responses can contain attacker-controlled text.

Prefer DOM text APIs:

js
const row = document.createElement('p')
row.textContent = `${message.from}: ${message.text}`
document.querySelector('#messages').append(row)

Do not concatenate untrusted values into innerHTML, inline event handlers, CSS, script source, or URL attributes. If rich text is essential, use a small audited sanitizer packaged with the extension and keep the result within the documented markup subset.

Never treat a nickname as authentication. Prefer the IRCv3 account tag when present, use hostmask only as a fallback, and design a safe behavior for unauthenticated senders.

Secrets

Store credentials in grove.secrets, not Web storage, HTML, logs, query strings, or package files. Validate endpoints separately and do not allow a URL to carry embedded username or password fields.

When sending a bearer token:

  • require TLS;
  • restrict the manifest to the exact origin where possible;
  • disable automatic redirects or revalidate the destination before following;
  • keep error messages and grove.log output free of headers and response bodies;
  • delete obsolete credentials from the Keychain namespace.

Settings pages have no network egress, which prevents a configuration form from transmitting a secret on its own. The running extension reads the saved configuration and performs the granted request.

Files

Upload providers never receive a local filesystem path or ambient directory access. Mango retains the security-scoped URL and services bounded range reads only for the selected file and active upload request. A provider cannot use the bridge to enumerate other files.

A composer utility with uploads.use may hand one bounded in-memory file to Mango only after the user invokes that utility. Mango materializes it in app-owned temporary storage, resolves the user’s configured upload destination for the current IRC session, uploads it through the same native workflow as an attachment, and inserts the returned URL into the draft. The extension never receives provider credentials or the resulting URL and cannot send the draft automatically.

The extension should still treat the filename, MIME type, file bytes, and server response as untrusted. Encode metadata according to the service protocol and do not build request headers by concatenating unchecked values.

Failure behavior

Denied or malformed bridge calls are logged and dropped. Message filters fail open on timeout, crash, rejection, or invalid output so a broken extension cannot silently consume communication. Uploads and secret operations return explicit failures to their callers.

Never solve a development problem by broadening capabilities, adding https://*, moving a secret into localStorage, or disabling origin validation without first establishing that the feature truly requires that authority.