Skip to content

Commit 1f0bb3a

Browse files
rgbkrkclaude
andcommitted
feat(blog): draft security post
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 52e418c commit 1f0bb3a

File tree

1 file changed

+109
-0
lines changed

1 file changed

+109
-0
lines changed

content/blog/security.mdx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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

Comments
 (0)