Toolbench

Security

The threat model, and what is in scope to report.

Reporting a vulnerability

Use GitHub's private vulnerability reporting. Please do not open a public issue for something exploitable.

Expect an acknowledgement within a week. This is a small project maintained in spare time, so a fix may take longer than that, and I will say so rather than go quiet.

Threat model

Read this before reporting. Several things that look like vulnerabilities here are documented design, and saying so up front is more useful than saying it in a reply.

A tool is first-party code. A tool is written by whoever runs the site and bundled at build time. There is no mechanism to load a tool from a URL at runtime, and that is deliberate: doing it on a static site means eval, which costs bundling, tree shaking, and any Content-Security-Policy worth having.

Worker mode is a stability boundary, not a security boundary. A worker-mode tool runs on its own thread so a runaway loop can be terminated without freezing the page. It shares the page's origin, cookies and storage. It is not a sandbox and the documentation never claims otherwise. If you need to run code you did not write, this is the wrong design and you want an iframe with a separate origin, or a server.

What a reader types is visible to the page that embedded the tool. <tool-host> attaches its shadow root in open mode, so the host page can already read every control through shadowRoot, and the values getter makes that supported rather than a reach into internals. So a tool is not a private place to put something: whatever a reader pastes, the surrounding page can read and send anywhere. That is the same trust level as any script on the page, and the host is first-party code by the paragraph above, but it is worth stating because these tools invite people to paste real data. If you need input the embedding page cannot see, this is the wrong design for the same reason worker mode is not a sandbox.

A link can fill a tool's form. With the share button on, which is the default for page and embed, a tool opens with the values its page's address carries (#227), so anyone who can send a reader a link chooses that reader's starting inputs. They go through the same clamping and refusal as typing, run the tool only as opening it would (an autoRun tool computes its first answer from them), and never reach an input marked share: false, or a secret input that does not say share: true; one rule decides that for both the link the button writes and the link a tool reads. What a tool does with a crafted input is the tool's concern, as it is for typed input. A host that does not want this sets share: "none".

A manifest checked at build time is marked, and the mark is a promise about origin, not a sandbox. @toolbench/runtime/vite and toolbench-manifests validate each tool.json when the site is built and emit it frozen at every level, carrying a non-enumerable Symbol.for("toolbench.validated"). RegistrySource uses a manifest that is both marked and frozen without validating it again. What the mark guarantees:

What it does not guarantee: script on the page can make its own frozen object with the symbol, because Symbol.for is a global registry. That script already runs with the page's authority, as the host is first-party code, so forging the mark gives it nothing it lacked. The element's own link check still runs on every link it draws, marked or not.

In scope, and worth reporting:

Out of scope:

Supply chain

Supported versions

Pre-1.0, only the latest minor gets fixes. Once 1.0 ships this section will say something more useful.