# Security

> hydration-proof security: no telemetry, requests only to the app under test, redacted reports, a locked-down local dashboard and one runtime dependency.

Source: https://hydration.jscrate.dev/docs/security
Last updated: 2026-09-18

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](https://github.com/re-sohail/hydration-proof/security/advisories/new),
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:

| What                                                            | Where it goes                                                                                             |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| The pages under test, and everything they load                  | The URL from `server.url` or `--url`, or the app hydration-proof started                                  |
| Readiness checks while the server starts                        | The same URL                                                                                              |
| Sitemap and crawl requests                                      | The 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:line` | The 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:

```text
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:

```ts title="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](https://hydration.jscrate.dev/docs/baselines). 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:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({
  redact: {
    patterns: [/CUST-\d{6}/],
    selectors: [".account-number", "[data-private]"],
  },
});
```

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `builtIn` | `boolean` | true | Remove emails, tokens, card numbers and secret URL parameters. |
| `patterns` | `RegExp[]` | — | More text to remove from reports. |
| `selectors` | `string[]` | — | 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:

```ts title="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](https://hydration.jscrate.dev/docs/scenarios) 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](https://hydration.jscrate.dev/docs/dev-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.

## Related

- [Scenarios and sign-in](https://hydration.jscrate.dev/docs/scenarios): test signed-in pages without
  committing credentials
- [Reports](https://hydration.jscrate.dev/docs/reports): what each output format contains
- [Detect hydration errors in CI](https://hydration.jscrate.dev/docs/ci)
- [Configuration reference](https://hydration.jscrate.dev/docs/configuration)
- [Troubleshooting](https://hydration.jscrate.dev/docs/troubleshooting)
