Security at nth Labs

Data privacy and security are fundamental to our mission, and to how this product is built. Every boundary below names the code that enforces it, and where a guarantee stops, this page says where.

Last updated 16 August 2026

Three questions about your data

A check is something a new route can forget to write. A directory is not. Two cross-tenant defects here had exactly the shape that choice exists to prevent.

Where can it go?

Only where it was allowed to. The browser fetching your URL sits inside the box holding your state and your key, so every address is judged before it moves and after it lands.

lib/urlguard.cjs · assertSafeUrl

Who can read it?

The account that filed it, and nobody else. Scoped by directory rather than by a filter each route has to remember, so there is no query to forget.

lib/tenant.cjs · stateDir

Whose key pays?

Yours, and never another customer’s: a file only you can read locally, and hosted, spent only on runs you started. Plan usage with no key of your own falls back to ours, metered.

lib/workspace.cjs · envVarsFor

Two of the eight checks send your product to a model

The two that read and reason send what a model has to look at: your page text, and the frames the browser captured. The other three send findings or nothing.

What gets sent What never moves

Leaves, on the runs that need a model

  • Page text from the pages the run opened on the product you pointed it at.
  • Screenshots of those pages, signed in if you handed over a test account, which means frames of a product nobody outside your team has seen.
  • Fresh frames when you ask it to verify a fix, with the selectors and text the surface is now made of, and only where re-running the recorded recipe in a browser did not settle it first.
  • To one provider, the one you configured: Anthropic, OpenAI, Gemini, Mistral or OpenRouter, under that provider’s own terms. The default model is OpenAI’s, so that is where a run goes until you choose otherwise.
  • The report’s written summary, and only if you hold a key. That one call sends the run’s own finding titles and scores, never a page and never a frame. The titles were written by a check that had read your pages, so some of that text is carried in them.

Never leaves, in any mode

  • Speed & errors and Re-checks call no model at all. Dead controls, Core Web Vitals, links, console errors, accessibility and the screen-by-screen visual diff are measured in a real browser and sent nowhere.
  • Your findings, frames and verdicts stay in your own account directory. They are never sold and never trained on.
  • No telemetry, no crash reporter, no usage ping, no licence check. Nothing here calls us on its own, which means there is no setting to find and turn off.
  • Card details. Credits are bought on Stripe’s own checkout page. What this console stores is the ledger entry your payment made; it never sees a card number.

Every address, and what the guard does with it

Not a summary of the rules. Every one of the 256 IPv4 blocks asked directly, sixteen times each, and coloured by the answer that came back.

IPv4 ADDRESS SPACE · 256 BLOCKS, EACH PROBED 16 TIMES refused always · 33 partly closed · 6 open · 217
0/8 reaches localhost on Linux 10/8 private 100.64/10 carrier NAT 127/8 loopback 169.254/16 link-local, and the metadata endpoint 172.16/12 private 192.168/16 private 224/4 multicast

Four gates, checked in order

Pointing a browser inward is how a testing tool becomes a way to read the machine it runs on. The path traced below is a real refusal.

The name you typed is not the target

A public hostname is free to resolve into private space, as often as its owner likes. So nth Labs judges every address the name resolves to, and one private answer refuses the run.

dns.resolve("yourproduct.com")3 ANSWERS
1104.21.9.44public
2172.67.180.12public
310.0.0.7private · run refused
4
01

The scheme, first

Only http and https reach anything. file:, gopher: and data: are refused outright: they are attempts to read the machine nth Labs runs on, not a product to test.

https:SCHEME OK
02

Then the name is resolved

A hostname is not the target. The addresses behind it are, and a perfectly public name is free to resolve into private space, so the check never judges the text you typed.

yourproduct.comRESOLVED
03

Every address it answers with

Not the first one. A name that returns one public address and one private one would otherwise be a coin flip its owner gets to flip.

2 answersBOTH PUBLIC
04

And again, wherever the browser landed

After every navigation the address is judged again, before the capture runs. Staying on the origin is allowed; leaving it for a private address is refused.

302 → 169.254.169.254REFUSED
CAPTURE NEVER RUNS · NOTHING KEPT

Where this stops. A DNS answer that flips between the check and the fetch is not caught: the browser driver does not expose the socket. The redirect case is, and test/browser/security.cjs proves it.

The same address, five ways to write it

Five notations of the cloud metadata endpoint, the whole prize in an SSRF. The guard parses addresses into bytes rather than matching text, because a regex loses to a spelling nobody thought of.

urlguard.isNeverAllowedALWAYS REFUSED
1169.254.169.254refused
2::ffff:169.254.169.254refused
3::169.254.169.254refused
40:0:0:0:0:ffff:a9fe:a9ferefused
5[::ffff:a9fe:a9fe]refused
6
7
8

Never reachable

Link-local, multicast and the unspecified address stay refused even when private space is deliberately opened, because nobody has ever legitimately pointed a product audit at the metadata endpoint.

urlguard.assertSafeUrlREFUSED WHEN HOSTED
1127.0.0.1local only
2::1local only
310.0.0.1local only
4192.168.1.1local only
5100.64.0.1local only
6
7
8

Reachable only locally

Local mode is somebody testing localhost:3000 on purpose, and refusing it would break the main way the tool is used. Hosted mode is a stranger on somebody else’s box, so private space closes.

nth Labs runs on your machine by default

The two modes are not a setting apart. They are different threat models, and the guard is stricter in the one where the URL came from a stranger.

On your machine

The console binds to the loopback interface, and hosting is a mode you turn on rather than a config value you can fat-finger. Your findings and your key are files only you can read.

localhost:3000allowed, on purpose
yourproduct.comallowed
169.254.169.254refused, even here

On the hosted console

The URL is untrusted input from any account, so private space closes and every address behind the name has to be public. Your findings and your key sit in your own account directory.

localhost:3000refused · not yours
yourproduct.comallowed
169.254.169.254refused, even here

Where this stops. Neither your model key nor your product’s sign-in password is encrypted at rest; owner-only permissions and the account directory protect them. Give the console a test account you can throw away.

In depth

Security practices

What the code does, by area. Every claim here is a line in a file you can read.

What we store, and for how long
Product security

A run dereferences a URL somebody else typed, in a browser, inside the box that holds every other account’s state. That is the whole threat model.

  • One of those callers has no account. The free check on the landing page runs from a public endpoint, so a URL can reach a browser here with no session behind it. Nothing the engine can do is narrower: one page, no crawl, deterministic checks only, no model key reachable from it. The fence around it is its own, the address is judged by the same guard below before anything launches, the child process is killed at ninety seconds, two may run at once, and three per address per hour. Its state is a scratch directory that is deleted when it finishes.
  • The name is resolved before it is judged. A public hostname may resolve into private space; loopback, link-local, the private ranges and the cloud metadata endpoints are refused, and one private answer refuses the whole run.
  • The address landed on is checked again. DNS can change between the check and the fetch, and a redirect can arrive somewhere the first gate never saw. The pre-flight check is the first gate, not the only one.
  • Model output is untrusted input. Every model answer is escaped at every render site, capped in length at the store boundary, and never interpolated into generated code.
  • Both surfaces send a Content-Security-Policy, the console with a per-response nonce, and it adds HSTS on a secure connection.
Data security

Scoping is a boundary rather than a filter each route has to remember. Two cross-tenant bugs here had exactly that shape, and both were routes that forgot.

  • One directory per account, not a row in a shared table. The id in the path comes from the system’s cryptographic random number generator and is validated on the way in, so no id can walk out of the accounts root.
  • State files are written owner-only, so nothing else on the box reads them.
  • The audit log refuses secrets by field name. A password, hash, token, key, cookie or session handed to it is dropped and the refusal written in its place.
  • An archive you move can be redacted, and the redactor names the fields it cleared rather than promising about a file it did not understand.

Where this stops: neither your model key nor your product’s sign-in password is encrypted at rest. Owner-only permissions and the account directory protect them. Give the console a throwaway test account.

Data privacy

Six of the eight checks never send your product anywhere. Two of them do, and the page says which, because a blanket privacy claim over a tool that calls a model is not true.

  • Speed & errors, Re-checks and Report run entirely in a browser on your machine. No API key, and nothing leaves.
  • Site review and Test drive send page text and screenshots to the model whose key you configured, and to nobody else. The provider is yours, the key is yours, and the bill is yours.
  • Findings are files, not rows in our database. On the default local install there is no server to send them to.
Access control
  • Passwords are salted scrypt hashes. A digest that is cheap to compute is cheap to attack; scrypt is slow and memory-hard on purpose.
  • Secrets are compared in constant time. Comparing with === leaks the prefix through how long the comparison took.
  • The session cookie is HttpOnly and SameSite=Strict, and carries Secure over HTTPS. The OAuth state cookie is scoped to the auth path alone.
  • A read-only viewer cannot write config, spend, or erase. That is a test, not a convention.
  • Sign-ins are rate-limited with an escalating lockout, and account creation is limited too. Each new account costs a directory and about a tenth of a second of scrypt, which is a denial-of-service primitive if it is free.
  • Signups on a hosted box are closed by default. An invite link is a one-time credential (checked for shape, workspace, expiry and prior use), and a cookie that is merely present opens nothing. The link is read at the form and only spent when an account is actually seated.
  • There is no second factor yet. A hosted account is a password, rate limiting and a lockout. Not a password and a phone. Invite-only signups and the throwaway-test-account rule are the fence around that gap until it closes.
Application security

The product’s pitch is that a claim nobody re-tested is not evidence. The same rule is turned inward here.

  • A guard must be seen to fail before it counts. The test is written against the unfixed code and watched going red; a test written after the fix proves the code passes the test, not that the test would have caught the bug.
  • A security fix ships as the exploit, inverted. The regression test is the attack that worked.
  • Every push runs a secret scan before anything else in the chain.
  • Roughly 1,650 tests across the engine, the console and the CLI, plus a browser suite driving real Chromium.
Infrastructure security
  • Local by default. The console binds to the loopback interface, and hosting is a mode you turn on rather than a config value you can fat-finger.
  • The two modes are different threat models, not a setting apart. The guard is stricter in the one where the URL arrived from a stranger.
  • No build step and no bundler. Plain CommonJS on Node 20+, so the dependency surface an attacker inherits is the one you can read in the lockfile.
The browser it drives

The endpoint that matters here is the browser a run steers, because that is the thing holding your product’s session.

  • A signed-in run uses a session you exported from your own browser, so the sign-in happened where you were. No password recipe, no bypass of your SSO or 2FA.
  • Every step it took is in the transcript, in the tester’s own words, next to the frame it was looking at when it took it.
Network and egress
  • Outbound: your product, and your model provider. There is no telemetry endpoint, and no analytics beacon on the console.
  • Inbound on the default install: nothing. Loopback only.
  • Inbound on the hosted console: the sign-in routes, Stripe’s webhook, and the free check. Everything else needs a session. The free check is the only one that does work for a caller with no account, which is why it is fenced the way the threat model above describes.
  • The site you are reading contacts nothing. Static HTML, no tag manager, no session recorder, no pixel, no cookie, and the fonts are served from this domain rather than a font CDN.
Where it is kept, and for how long

Run it yourself and the answer is your disk, in whatever country your disk is in. The hosted console is the case worth stating.

  • There is no automatic retention window. Until you delete something, it is kept. The one thing that outlives an account is its billing ledger, because that a payment happened has to survive seven years for bookkeeping, though everything it paid for is gone.
  • Backups are nightly where the operator has configured them, encrypted before they leave the box, kept for the last thirty nights plus one a month for a year.
  • Deletion reaches the backups only on request. For up to a year after you delete something a copy exists inside a backup archive: not readable by the console, not selectively restorable, and touched only if the whole service has to be rebuilt. Ask and it is done by hand.
  • The hosted console runs on Railway; payments go to Stripe; these pages come off Vercel; model calls go to whichever provider the run’s model belongs to. nth Labs is operated from Toronto, so data on the hosted console is stored and processed outside Canada, in the United States.

The full table, including the lawful basis for those transfers, is on the privacy page.

If something goes wrong
  • You get an email, not a status page. The address on your account, with what happened, what of yours was in it, what has been done and what you should do.
  • Inside seventy-two hours to the relevant authority, where the UK and EU rules put that clock on it. We would rather send an incomplete account inside it than a tidy one afterwards.
  • The same address takes reports in the other direction. If you find a way into somebody else’s data here, write to us.
What there is not

No SOC 2 report, no ISO certificate, no independent penetration test, and no third-party audit of anything above. There is nothing to point at yet, and we would rather write that than imply one.

Neither key material nor stored passwords are encrypted at rest. There is no bug bounty. If you need either before adopting a tool, we would rather you knew now.

Found a way into somebody else’s data here? hello@nthlabs.dev, the same address the privacy page gives. We will answer, and we will say what we fixed.