Hydration Proof

Search documentation

Find a page or section

What the tool sees, what it sends where, and how to keep reports safe to share.

hydration-proof security starts from one fact: the tool runs your app in a browser and reads everything it renders, including pages behind a login. It uploads none of it. There is no telemetry, every request goes to the app you are testing, and reports are redacted before they are written.

Report a hydration-proof security issue

Report vulnerabilities privately through GitHub security advisories, not in public issues. Include the version, the command you ran and, if you can, a fixture or app that shows the problem. You get a first reply within a week. Security fixes are released for the latest minor version.

No telemetry

Nothing about your app, your run or your machine is uploaded. There is no analytics, no crash reporting, no version check and no "anonymous usage data", neither on install nor at run time. The package has no postinstall or other lifecycle script, so nothing runs until you run a command yourself.

What the network sees

Every request comes from a command you started, and all of them go to the app you are testing:

WhatWhere it goes
The pages under test, and everything they loadThe URL from server.url or --url, or the app hydration-proof started
Readiness checks while the server startsThe same URL
Sitemap and crawl requestsThe same origin. Crawling never leaves it; a routes.sitemap URL you configure yourself is used as given
Scripts and source maps, to turn a stack frame into file:lineThe app's origin, plus any origin in sourceOrigins

Two commands reach further, and only when you run them:

  • hydration-proof install downloads browsers through Playwright's own installer and its hosts. PLAYWRIGHT_DOWNLOAD_HOST and the other Playwright environment variables work as usual, so you can use an internal mirror.
  • The build and server commands from your config run as you wrote them, with your environment. They can do anything.

Why source maps are origin-restricted

To name the file and line a finding comes from, hydration-proof downloads the page's scripts and their source maps. Those requests go only to the app's own origin. A script or sourceMappingURL that points anywhere else is skipped, and the run ends with a note that lists the origins it refused:

Source maps were not fetched from https://cdn.example.com (only the app's own
origin is used). Add sourceOrigins to the config to allow them.

This matters because a page can name any URL in a sourceMappingURL comment. Without the restriction, testing a page you do not control could make your CI runner fetch from a host of that page's choosing, from inside your network. If your app serves its bundles from a CDN, allow that one origin:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  sourceOrigins: ["https://cdn.example.com"],
});

Findings are still reported when a map is skipped, only without a source location. The restriction costs you file names, never detection.

What ends up in a report

A report is built from the app's own output, so it can contain page text, attribute values, URLs with their query strings, server log lines and screenshots. Treat a report like any other test artifact of the app it came from, and think before you attach one to a public pull request.

Redaction is on by default

Before anything is written or printed, known secrets and personal data are replaced. The built-in rules cover:

  • email addresses
  • JWTs and Bearer tokens
  • API keys: sk_, pk_ and rk_ keys, GitHub (ghp_, github_pat_), GitLab (glpat-), Slack (xox…), AWS (AKIA…) and Google (AIza…)
  • PEM private key blocks
  • secret URL parameters (token, api_key, password, session, code, signature and others), whose values become [redacted]
  • payment card numbers. A match must pass the Luhn checksum and have a prefix and length a card network issues, so order ids and millisecond timestamps are left alone.

Fingerprints are computed first, so redaction never changes which findings are grouped together or matched against a baseline. The summary of a run says how many values were replaced and under which label, so you can see the rules firing.

Add your own patterns, and black out elements in screenshots:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  redact: {
    patterns: [/CUST-\d{6}/],
    selectors: [".account-number", "[data-private]"],
  },
});
OptionTypeDefaultDescription
builtInbooleantrueRemove emails, tokens, card numbers and secret URL parameters.
patternsRegExp[]More text to remove from reports.
selectorsstring[]Elements to black out in screenshots.

selectors only masks screenshots; it does not change report text. redact: false turns redaction off entirely, for example for local runs. screenshots: "off" is the blunt instrument for pages whose pictures should not exist at all.

Keep credentials out of the config

Never write credentials into hydration-proof.config.ts. Sign in with a scenario's login, reading the credentials from process.env, or with a Playwright storageState file. A token can go in a header:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  scenarios: [
    {
      name: "admin",
      headers: { authorization: `Bearer ${process.env.TEST_TOKEN}` },
    },
  ],
});

Seed test data in hooks.setup, which runs after the app is up and before any page is tested, and read its secrets from the environment as well. hydration-proof init adds the report folder to .gitignore; add your storageState file too. Signed-in pages covers each way to sign in.

Is the HTML report safe to open?

report.html is a single file you open from disk. The report data sits in a <script type="application/json"> element with <, >, & and the line separators escaped, and the viewer only writes it into the page with textContent. Page content from the app under test cannot become markup or script in the report.

The document also carries a Content-Security-Policy meta tag with default-src 'none', which stops the report from loading or sending anything at all.

The dashboard and the dev overlay

hydration-proof ui starts a small local server, meant for your own machine. It is built so that a web page you happen to have open cannot drive it:

  • it listens on 127.0.0.1 only, never on 0.0.0.0
  • every request needs a random 144-bit token, generated per run and printed as part of the URL; it is compared with timingSafeEqual
  • requests whose Host is not 127.0.0.1:<port> or localhost:<port> are refused, so a DNS name that resolves to your loopback address cannot reach it
  • POST /api/* needs the token in an x-hydration-proof-token header, which a cross-site form cannot set, and a request with an Origin header from anywhere else is refused
  • report files are served from one token-prefixed path, from inside the output folder only, and only with known media types
  • bodies are capped, and responses carry no-store and nosniff

Do not forward the port or share the URL. The token in it is the only thing protecting the dashboard, and running tests from it means running your configured build and server commands.

hydration-proof dev opens a normal browser window with the overlay injected by an init script. The overlay lives in its own shadow root, so the app's CSS cannot reach it and its CSS cannot reach the app. Every node it adds carries data-hydration-proof-internal, so the tool's own markup is never compared as if it were the app's. Nothing is written into your source tree, and nothing ships in your production bundle.

Supply chain

  • One runtime dependency: playwright-core, which itself has none. Everything else is Node.js built-ins or code in the repository.
  • No lifecycle scripts: the package is published as ESM without install scripts, so installing it cannot execute anything.
  • Nothing loaded at run time: the browser runtime is inlined into dist at build time, so nothing is read from disk or the network to inject it. This is also what makes Yarn Plug'n'Play work.
  • Provenance: releases are published from CI with npm trusted publishing (OIDC), so no long-lived npm token exists to leak. Every published version has a provenance attestation you can check with npm audit signatures.

Running it against untrusted pages

Testing a URL you do not control means loading that page in a browser on your machine or runner. The browser is Playwright's, sandboxed as usual. Each run gets a fresh context, and the source-map restriction keeps a hostile page from steering requests.

It is still worth running it in a container with no access to your internal network, the way you would treat any other untrusted URL.