|
| 1 | +--- |
| 2 | +title: "How nteract thinks about security" |
| 3 | +description: "Notebooks have historically been a security free-for-all. We're changing that — iframe isolation, dependency trust, sandboxed runtimes, and more." |
| 4 | +date: 2026-04-07 |
| 5 | +published: false |
| 6 | +tags: |
| 7 | + - nteract |
| 8 | + - security |
| 9 | + - architecture |
| 10 | +--- |
| 11 | + |
| 12 | +Notebooks are weird. They're documents that execute code. You open a file someone sent you, and suddenly it wants to install packages, run arbitrary Python, and render HTML with JavaScript — all on your machine. |
| 13 | + |
| 14 | +For most of notebook history, the security model has been: **there isn't one.** Jupyter's trust system checks whether *you* generated the outputs, but the actual execution? Wide open. Any notebook can run any code, install any package, and access anything your user account can. |
| 15 | + |
| 16 | +We think that's worth fixing. |
| 17 | + |
| 18 | +## Every output is isolated |
| 19 | + |
| 20 | +Here's something most notebook apps don't do: isolate outputs from the application itself. |
| 21 | + |
| 22 | +In nteract, every cell output — HTML, images, plots, widgets, SVG, markdown — renders inside an iframe with an [opaque origin](https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-opaque). The iframe gets a `blob:` URL, which means it has no origin relationship to the parent window. It can't see the parent DOM. It can't access `localStorage`. And critically, it can't reach `window.__TAURI__` — the bridge that would give it access to your filesystem, shell, and native APIs. |
| 23 | + |
| 24 | +The sandbox attributes are deliberately restrictive: |
| 25 | + |
| 26 | +``` |
| 27 | +allow-scripts |
| 28 | +allow-downloads |
| 29 | +allow-forms |
| 30 | +allow-pointer-lock |
| 31 | +allow-popups |
| 32 | +allow-popups-to-escape-sandbox |
| 33 | +allow-modals |
| 34 | +``` |
| 35 | + |
| 36 | +The one that's **not** there is the important one: `allow-same-origin`. If we added that single attribute, the entire isolation model collapses — the iframe would share the parent's origin and gain full access to Tauri APIs. This invariant is tested in CI. It will never ship. |
| 37 | + |
| 38 | +What does this mean in practice? If someone sends you a notebook with a cell that outputs: |
| 39 | + |
| 40 | +```html |
| 41 | +<script> |
| 42 | + // Try to access the host application |
| 43 | + window.__TAURI__.invoke('execute_command', { cmd: 'rm -rf /' }); |
| 44 | +</script> |
| 45 | +``` |
| 46 | + |
| 47 | +It simply doesn't work. `window.__TAURI__` is `undefined` inside the iframe. The script runs in a sandboxed void with no way out. |
| 48 | + |
| 49 | +### Widgets work too |
| 50 | + |
| 51 | +You might wonder — if outputs are isolated, how do interactive widgets work? They need to communicate with the kernel to send and receive state updates. |
| 52 | + |
| 53 | +We built a JSON-RPC 2.0 bridge over `postMessage`. The parent window owns the widget state (stored in an Automerge CRDT document, synced with the daemon). The iframe gets a proxy that can read and update model state, but only through the validated message channel. The iframe never gets direct access to the kernel, the daemon, or any Tauri API. |
| 54 | + |
| 55 | +Widget state updates are validated, typed, and routed through a `CommBridgeManager` that acts as a gatekeeper. Even [anywidgets](https://anywidget.dev/) — third-party widgets that load ESM modules at runtime — run inside this same isolation boundary. |
| 56 | + |
| 57 | +### Content Security Policy |
| 58 | + |
| 59 | +The iframe also enforces a Content Security Policy. External resources (scripts, stylesheets, widget ESM modules) must load over HTTPS — no plain HTTP. This means anywidgets that fetch code from the web are required to use secure connections. |
| 60 | + |
| 61 | +For organizations with stricter requirements — locking down `connect-src`, restricting script sources to specific domains, disabling `unsafe-eval` — these are levers we'd like to expose. If tighter output policies are something your team needs, [we'd love to hear about it](https://github.com/nteract/desktop/issues). |
| 62 | + |
| 63 | +## Trust before install |
| 64 | + |
| 65 | +Notebooks with inline dependencies are one of our favorite features. Store your `pandas`, `numpy`, whatever right in the notebook metadata. Send it to a colleague and they can run it immediately. |
| 66 | + |
| 67 | +But "immediately" is dangerous. That's code you didn't write, asking to install packages you didn't choose, on your machine. |
| 68 | + |
| 69 | +nteract won't install anything without explicit approval. When you open a notebook with dependencies, the runtime doesn't start. You see the full package list. You click "Trust & Start." Only then does installation begin. |
| 70 | + |
| 71 | +The trust signature is an HMAC-SHA256 over the dependency metadata, signed with a key that lives only on your machine (`~/.config/runt/trust-key`, permissions `0600`). If anyone — human or agent — modifies the dependencies after you've approved them, the signature invalidates and nteract asks again. |
| 72 | + |
| 73 | +We also run typosquatting detection. If a notebook asks for `reqeusts` instead of `requests`, you'll see a warning. It's not perfect, but it catches the obvious supply chain attacks that prey on typos. |
| 74 | + |
| 75 | +## The daemon is local-only |
| 76 | + |
| 77 | +The nteract daemon (`runtimed`) communicates over a Unix socket, not a TCP port. The socket has owner-only permissions (`0600`), so only your user account can connect. |
| 78 | + |
| 79 | +There's no HTTP server to accidentally expose. No port to scan. No CORS headers to misconfigure. The attack surface for remote exploitation is effectively zero — there's nothing listening on the network. |
| 80 | + |
| 81 | +The connection protocol starts with a magic byte preamble (`0xC0DE01AC` + version byte) before any handshake. Non-runtimed clients get rejected immediately. Frame sizes are capped (64 KiB for control frames, 100 MiB for data) to prevent resource exhaustion. |
| 82 | + |
| 83 | +The one web server that does run is the blob store — a read-only HTTP server bound to `localhost` that serves images, Arrow data, and other binary outputs. It's read-only and local-only. It exists because iframes need a way to fetch binary content, and `blob:` URLs can't do cross-origin requests. |
| 84 | + |
| 85 | +### What about agents? |
| 86 | + |
| 87 | +Any process with filesystem access to the Unix socket can connect to the daemon. That's by design — it's how MCP-based agents (Claude Code, Codex, Warp, etc.) join your notebooks as realtime collaborators. |
| 88 | + |
| 89 | +This is the same trust model as SSH: if you can access the socket file, you're authorized. Agents that run on your machine, as your user, get the same access you have. It's a deliberate trade-off. We're not trying to protect you from tools you chose to run — we're trying to protect you from untrusted content inside notebooks. |
| 90 | + |
| 91 | +## The philosophy |
| 92 | + |
| 93 | +Notebooks are collaboration documents now. Humans edit them, agents edit them, kernels write outputs into them. The security model has to account for all of these actors. |
| 94 | + |
| 95 | +Our approach is defense in depth: |
| 96 | + |
| 97 | +1. **Outputs can't escape their iframe.** Even if a kernel produces malicious HTML, it can't touch the host app. |
| 98 | +2. **Dependencies require explicit trust.** No silent installs, no unsigned packages slipping through. |
| 99 | +3. **The daemon has no network surface.** Unix sockets with restricted permissions. |
| 100 | + |
| 101 | +None of these are revolutionary ideas individually. But notebooks have operated without *any* of them for over a decade. We think it's time to raise the bar. |
| 102 | + |
| 103 | +## What's next |
| 104 | + |
| 105 | +* [Runtime sandboxing](https://github.com/nteract/desktop/issues/1307): OS-level process isolation for kernel subprocesses, so untrusted code runs with only the access it needs — project files, packages, and localhost. Opt-in at first, with the long-term goal of sandboxing agent-initiated sessions by default. |
| 106 | +* [Remote runtimes over SSH](https://github.com/nteract/desktop/issues/1334): run kernels on remote machines, tunneled through SSH. No new auth systems, no exposed ports. |
| 107 | +* [Scoped Tauri capabilities](https://github.com/nteract/desktop/issues/908): each window gets only the native permissions it actually needs, not the full set. |
| 108 | + |
| 109 | +<BlogCTA /> |
0 commit comments