md

Trust and network notes

md is a Markdown editor that runs in your browser. This page is for the person deciding whether to allow it. It lists what exists, how to check it, and where the limits are. Nothing here is a promise that cannot be tested.

What to allow

One hostname: md.ashwingopalsamy.in. No other host is contacted, ever. The page's policy has no entry for any other host, so the browser refuses a request to one before it leaves the machine.

What requests exist

What TLS inspection will see

The app's own files, going down. Never a document, because the page has no way to send one. There is no upload, no sync and no save-to-server. Documents are kept in the browser's IndexedDB on the device and written to files you choose.

What is not there

The policy

Sent as a header and repeated as a <meta> tag at the top of the page, so a copy saved to any static server has it too. A meta tag cannot carry frame-ancestors, so that one is header-only.

The app page, as a header:

default-src 'none';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'none';
worker-src 'self';
manifest-src 'self';
object-src 'none';
frame-src 'self';
base-uri 'none';
form-action 'none';
frame-ancestors 'none'

The same policy in the page's meta tag has no frame-ancestors:

default-src 'none';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'none';
worker-src 'self';
manifest-src 'self';
object-src 'none';
frame-src 'self';
base-uri 'none';
form-action 'none'

The guide pages, including this one, have a stricter policy with no inline styles:

default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'none';
manifest-src 'self';
object-src 'none';
frame-src 'none';
base-uri 'none';
form-action 'none';
frame-ancestors 'none'

The service worker, /sw.js, has its own policy. Its only fetches are this site's files while it installs:

default-src 'none'; connect-src 'self'

These headers are sent on every path: X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, a Permissions-Policy that denies camera, microphone, location and the other device features, Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp, X-Frame-Options: DENY (except on the engine frames, which the page holds), X-DNS-Prefetch-Control: off, and Strict-Transport-Security on the hosted site. The exact values are in dist/trust.json under policy.

The one trade-off: inline styles

style-src includes 'unsafe-inline' on the app page. KaTeX writes style attributes on its math, and Mermaid writes a <style> element inside its diagrams, so a policy without it would break both.

It cannot be used to send anything out. CSS can only reach the network through url(), @import or a font, and the policy closes each of those to this site: img-src allows 'self', data: and blob:, font-src allows 'self' and data:, and there is no connect-src. A rule that names an outside address is blocked by the browser. Style attributes also cannot run script.

How to check each claim

Each of these can be run by someone who does not trust this page.

  1. Browser's own record. In md, open the Privacy sheet. It counts the network entries the browser recorded since the page loaded, shows the policy read from the page, and lists any policy violations the browser reported. You can compare the count with the Network tab of the developer tools.
  2. Live self-test. The same sheet has a button that tries fetch, XMLHttpRequest, WebSocket, sendBeacon, a web picture and a frame, a fetch from inside a diagram engine's frame, all to https://blocked.invalid/, and runs a set of script-injection payloads through the cleaner. It shows each one refused. The .invalid domain never resolves, so nothing could leave even if a browser ignored the policy.
  3. Static audit. npm run build runs tools/audit.mjs on the output. It fails the build on an outside address in a src or href, an inline script, a script from another origin, a WebAssembly file, an assignment to document.cookie, or a known analytics host. It also counts the network-capable APIs in every JavaScript file: fetch(, XMLHttpRequest, WebSocket, EventSource, sendBeacon, importScripts, eval(, new Function, Function(, navigator.connection and WebAssembly. Each match must be on a reviewed list, tools/audit-allow.json, with a reason, so a new one fails the build. Outside addresses that appear as text in library code, such as XML namespaces, are reported and not treated as failures. Files under /engine/ must be exactly the engine files md ships: the frames' own, @plantuml/core's two at the locked version, and the vendored files whose SHA-256 sums are in vendor/SHA256SUMS.
  4. Compare what is served. The audit writes dist/trust.json: the SHA-256 and size of every file, the policy, the production dependencies with their versions and licences, and the audit findings. There is no build time in it, so the same source gives the same file. npm run verify:site fetches every file from the live site and compares hashes, and checks that the page's header has connect-src 'none'.
  5. Read the source. It is a small repository. Its one server, in local/, belongs to md Local and is not part of this site: the audit fails a hosted build that names its API. The repository is at github.com/ashwingopalsamy/markdown.

The service worker

sw.js is registered by the page and keeps every file the app can ask for in the browser's Cache Storage. It answers requests for this site's files from that copy. It never fetches anything for a document, and it makes no request except to this site's files while it installs. A new version waits until the old one's windows are closed, so an open tab never loses a file halfway; the page can ask to update when it is the only window. Its policy is shown above.

Diagram engines

PlantUML and draw.io drawings are made in sandboxed frames from /engine/. A sandboxed frame has no origin, so it cannot read the page or its storage. Its policy has connect-src 'none', so it cannot reach the network at all, not even md. The page sends it the diagram's text and gets back a drawing, which is cleaned before it is shown. draw.io's shape sets come from md: the page loads the ones a drawing needs as scripts from this site and passes them in.

The PlantUML frame runs WebAssembly (Graphviz, inside @plantuml/core), so its policy allows 'wasm-unsafe-eval'. The page's policy does not.

The app page allows frames from this site only (frame-src 'self'), and only the engines use it.

default-src 'none';
script-src 'self' 'wasm-unsafe-eval';
style-src 'unsafe-inline';
img-src data:;
font-src data:;
connect-src 'none';
sandbox allow-scripts;
frame-ancestors 'self'

Their scripts load with crossorigin and are sent with Access-Control-Allow-Origin: *, because a sandboxed frame has no origin to match. They are public code with no secrets.

Run it on localhost instead

If the hosted site cannot be allowed, run the same files on your own machine. No hostname beyond 127.0.0.1 is involved after you have them.

git clone https://github.com/ashwingopalsamy/markdown
cd markdown
npm ci
npm run local

That builds and serves dist/ at http://127.0.0.1:4873/. The server is a short file, tools/serve.mjs, with no dependencies: it answers GET and HEAD only, refuses a Host header that is not loopback, applies the same headers as the hosted site except HSTS, and logs no paths. Documents on 127.0.0.1 and on the hosted site are kept separately, because browser storage belongs to an origin. md Local, built by npm run build:local, is a different edition: its own server reads and saves files on disk, and it has its own policy and its own trust page.

Limits, said plainly

Open the editor