← 03 — Blog

A web tool that can't phone home: connect-src 'none'

Tooling2 min read

“Never paste secrets into a website” is good advice, and it is exactly what a browser-based password generator asks you to ignore. You cannot read every line of a page’s JavaScript before you use it, and you cannot know that the code you read yesterday is the code you got today. When we built passwd.entropywatch.dev, the question was how to make “nothing leaves your browser” something you can check rather than something you take on trust.

Why a promise is not enough

A web page has many ways to send data out: fetch(), XMLHttpRequest, WebSockets, navigator.sendBeacon(), an image whose URL carries the data, a form submission. A privacy statement covers none of them. Even a careful developer can ship a dependency that adds one.

What we did instead

The generator’s origin sends a Content Security Policy that forbids the network outright:

Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self';
  img-src 'self'; font-src 'self'; connect-src 'none'; form-action 'none';
  frame-ancestors 'none'; base-uri 'none';
  require-trusted-types-for 'script'; trusted-types 'none'
  • connect-src 'none' blocks fetch, XMLHttpRequest, WebSockets, EventSource and sendBeacon — to every destination, including the page’s own origin.
  • img-src, font-src, script-src and style-src allow only 'self', so an image or script URL cannot carry data to another host.
  • form-action 'none' stops form submissions.
  • Trusted Types with no allowed policy means no string can be turned into HTML or script, which closes the usual injection route for anything that would try to get around the above.

The policy is a response header, so the browser enforces it regardless of what the JavaScript wants. The tool also runs on its own subdomain, with nothing else on that origin to share storage or loosen the policy.

How to check it yourself

Open DevTools, reload the page and look at the response headers of the document: the policy above should be there. Then click Run self-test on the page. It deliberately calls fetch(), new Function() and trustedTypes.createPolicy(), and reports which of them the browser blocked; the console shows the matching CSP violations. The header also counts third-party requests since the page opened, from the browser’s own resource timing data.

What CSP does not cover

CSP does not restrict top-level navigation. Code that sets location to another site with data in the URL would not be blocked. Our defense there is size: the generator is one small script you can read in the Sources panel. If you want the strongest guarantee, load the page and disconnect from the network before you generate anything — it keeps working.

Takeaway

The same header is worth using for internal tools that handle sensitive data: log viewers, IOC decoders, anything an analyst pastes material into. If the tool never needs the network, say so in the CSP and let the browser hold it to that. For how long the passphrases themselves should be, see How long should a passphrase be?