Murasaki

Security

Murasaki's trust boundaries, built-in runtime protections, and application responsibilities.

Desktop security starts with process boundaries. Murasaki keeps Node out of the renderer, restricts the application WebView to its own origin, and protects privileged loopback endpoints with an app-session token. These controls reduce the impact of ordinary renderer bugs, but they are not a complete permission system.

Native renderer commands are deny-by-default and granted with the typed capabilities list. Every declared window has its own allowlist, but argument-level and path/URL scopes are not available. Treat every renderer-to-Node function as privileged application code and keep its surface small.

Trust boundaries

BoundaryBuilt-in protectionYour responsibility
Remote URL → app WebViewExact app-origin navigation stays in-window; off-origin HTTP(S) opens in the system browser; file:, data:, and javascript: navigations are deniedDo not weaken navigation behavior or inject remote scripts into the renderer
Renderer → Rust native commandsExact-origin IPC check and a typed, deny-by-default allowlist evaluated for the calling windowGrant only the commands that renderer uses; validate app-level intent before broad operations such as window:manage
Renderer → local NodePrivileged /api/* and /__murasaki/* endpoints require a random 256-bit HttpOnly, SameSite=Strict runtime cookie and validate Host/Origin; wire payloads are boundedValidate arguments and authorize the requested operation inside every action/route/Main function
OS URL/file activation → Node MainOnly configured schemes/extensions are normalized into openRequested() targets; delivery uses the authenticated app-local channelTreat every URL and path as attacker-controlled; validate intent, identifiers, hosts, and file contents before acting
App → update hostEd25519 signature over raw manifest bytes, then SHA-256 verification of the selected payloadProtect the private update key and publish hashes/signatures together
OS → installed appmacOS Developer ID/notarization and Windows Authenticode orchestration sign and verify final app-owned artifactsSupply and protect your platform certificate/provider; test trust policy and installed artifacts on clean machines

Production listens on 127.0.0.1, not all interfaces. Development accepts loopback localhost, 127.0.0.1, or [::1] hosts and rejects cross-site Fetch Metadata. Static document responses install the session cookie; it is not readable from JavaScript.

Grant native commands explicitly

murasaki.config.ts
export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',
  window: {
    route: '/',
    capabilities: ['app:quit', 'dialog:openFile', 'shell:openExternal', 'window:open'],
  },
  windows: {
    preview: { route: '/preview', capabilities: ['clipboard:writeText'] },
    settings: { route: '/settings', capabilities: [] },
  },
})

Omitting capabilities grants nothing, except that the primary window falls back to the legacy top-level list when window.capabilities is absent. Secondary windows never inherit that list. Unknown strings grant nothing, and the bridge accepts calls only from the exact trusted application origin. The allowlist is command-level: it cannot constrain a dialog to one directory or an external URL to one host. window:manage can target any declared window, so keep it on a trusted renderer and enforce narrower intent in app code. Application-wide programmatic shutdown is separately gated by app:quit.

Keep secrets in Node

Anything imported by renderer code can become public client JavaScript. Keep API tokens, private keys, database credentials, and license validation in src/main.ts, 'use main', 'use server', or server-only API route modules.

src/backend/account.ts
'use main'

export async function loadAccount(accountId: string) {
  if (!/^[a-z0-9_-]{1,64}$/i.test(accountId)) {
    throw new TypeError('invalid account id')
  }

  const token = process.env.ACCOUNT_API_TOKEN
  if (!token) throw new Error('account service is not configured')

  // Call the remote service from Node. Return only what the renderer needs.
  const response = await fetch(`https://api.example.com/accounts/${accountId}`, {
    headers: { authorization: `Bearer ${token}` },
    signal: AbortSignal.timeout(10_000),
  })
  if (!response.ok) throw new Error(`account service returned ${response.status}`)
  return response.json()
}

The runtime session proves that a request came through the app's renderer origin. It does not prove that the renderer is uncompromised. XSS can still invoke any function that your client bundle can invoke.

Validate paths and URLs

Never concatenate renderer input into an unrestricted filesystem path. Resolve it beneath an application-owned directory and verify that the result stays inside that directory. Prefer opaque IDs over raw paths.

Likewise, allowlist protocols and hosts before a Node function calls fetch() or opens a URL. This prevents accidental local-network access and SSRF. Do not forward arbitrary request headers from the renderer to a remote service.

The same rule applies to openRequested(). A registered scheme is a dispatch mechanism, not an authentication mechanism: any local process, browser, or document can attempt to open it. File associations also identify a path, not a trusted file.

src/main.ts
import { defineMain } from 'murasaki/main'

export default defineMain({
  async openRequested(_context, event) {
    for (const target of event.targets) {
      if (target.kind === 'url') {
        const url = new URL(target.url)
        if (url.protocol !== 'example-notes:' || url.hostname !== 'open') continue
        const id = url.pathname.slice(1)
        if (!/^[a-z0-9_-]{1,64}$/i.test(id)) continue
        // Resolve the validated identifier in application code.
      } else {
        // Check the expected extension, access policy, size, and file format
        // before parsing target.path. Do not execute content based on its name.
      }
    }
  },
})

Do not automatically navigate the renderer to an arbitrary deep-link URL, interpolate its query values into HTML, or execute a file because it carries a registered extension. If the renderer needs the result, send a narrow, validated value through the Main event API.

Content Security Policy

Murasaki sets X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer on the document and injects one CSP meta tag into framework-owned and user-owned HTML. The production default uses script-src 'self', blocks objects, frames, base URL changes, inline script attributes, and form posts to other origins, while allowing remote-backed applications to connect over HTTPS/WSS. Images, fonts, and media support the common HTTPS/data/blob sources. style-src 'self' 'unsafe-inline' remains for React style attributes and runtime CSS compatibility.

Development uses a separate policy: it additionally permits inline scripts inserted by Vite/React Refresh and ws: connections for HMR. It does not add 'unsafe-eval'. This development relaxation is not present in production.

Override the complete policy, or opt out when another layer owns it:

export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',
  security: {
    csp: "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'none'; frame-src 'none'",
  },
})

// Disable only Murasaki's injection:
// security: { csp: false }

A string is a complete override, not a directive merge. If a user-owned index.html already has a CSP meta tag, Murasaki keeps it and moves it to the beginning of <head>. Setting both that tag and a security.csp string is a build error instead of silently choosing one. Use the browser console's CSP violation to identify a blocked source, then add only that source or move the resource to the app origin.

When migrating an existing app, move production inline scripts into external modules. Remote scripts and frames are blocked by default and must be enabled with an explicit override. HTTPS/WSS backends work with the default; plain-HTTP remote backends need an override (same-origin loopback calls remain allowed).

Meta-delivered CSP has browser-defined limits. It begins applying where the tag is parsed (Murasaki prepends or moves it to <head>), and directives such as frame-ancestors, sandbox, and reporting are not fully supported from a meta tag. They are therefore not included in the default. Use response headers for a real network service that needs those protections. CSP also does not sanitize HTML or authorize Node functions, and the default inline-style allowance is a compatibility tradeoff rather than an XSS guarantee. Continue to avoid dangerouslySetInnerHTML, sanitize user-authored HTML, and pin third-party code.

Exposing a real network service

Murasaki API routes are app-local by design and are not reachable as a public service. A Main process can create TCP, WebSocket, or HTTP listeners using Node, but doing so creates a new security boundary outside Murasaki's runtime-session protection.

  • Bind to 127.0.0.1 unless remote clients are an explicit product feature.
  • Authenticate before processing messages.
  • Define message and payload limits.
  • Rate-limit expensive operations.
  • Close the listener in shutdown().
  • Never reuse the Murasaki runtime token as your protocol credential.

Signing and update keys are different

Code signing answers “who produced this executable?” to the operating system. The Ed25519 update signature answers “did the holder of this app's update key authorize this manifest?” to Murasaki. A production updater should use both: Developer ID/notarization or Murasaki's Windows Authenticode --sign flow for the OS artifact, plus Murasaki's manifest signature and payload hash.

Known security gaps

  • Native capabilities are command-level and per-window, but there are no path/URL/argument scopes or separate policy files.
  • Declared windows share one application origin and authenticated local Node session. Native allowlists do not isolate Server Actions, 'use main', API routes, or updater endpoints; authorize those operations in application code.
  • Linux package signing is not implemented. Windows Authenticode requires a developer-supplied certificate/Artifact Signing provider and Windows SDK SignTool.
  • Linux protocol/file-association registration is not implemented. Windows portable archives intentionally do not self-register.
  • Renderer native APIs are not callable directly from src/main.ts.

Track exact status on Platform & feature status.

Report a vulnerability

Do not open a public issue. Use a private GitHub Security Advisory or email [email protected]. The latest published minor is the supported pre-1.0 line.

Next

On this page