Free exposed secrets checker
Find API keys, tokens and private keys shipped in your page's own JavaScript, and see exactly which files we could not read, no signup.
We read one page, in one state, without signing in. Being clear about the edges is the point: a scanner that implies it sees everything is the one to distrust. See exactly what we can't see →
- 11 checks each pass, fail, or needs review
- Under a minute one page, one scan
- No signup no account, no email
What we check
Stripe secret key
sk_live_ then 10 to 300 characters drawn from A-Za-z0-9The live secret prefix, and only that prefix. Publishable keys are the ones meant to sit in your front end: they begin pk_live_ or pk_test_, so none of them can match this pattern at all, and sk_test_ is passed over on the same grounds. The 300 ceiling is headroom against a future lengthening rather than a limit any key reaches today. Scored critical. Findings are aggregated per credential type and deduped on type plus file, so a minified bundle that repeats one key forty times produces a single row naming that file once, never forty rows.
Anyone who opens the bundle has your Stripe account through its API: charging a card, sending a refund to a destination they pick, and reading back the customer and payment objects behind your checkout are all just requests with that key attached.
Learn more about Stripe secret key →AWS access key ID
AKIA immediately followed by exactly 16 characters from A-Z0-9Fixed width, not a range: sixteen uppercase alphanumerics after the prefix, bounded by word boundaries at both ends, so an identifier of any other length is not this. AKIA alone is matched, and no other four-letter identifier prefix is. What we report is the identifier; the secret half of the pair is not something this scan looks for, and since the identifier is inert without it, one compiled into a bundle is worth treating as a leak of both until you have opened the file and read what sits beside it. Scored critical.
The identifier tells an attacker which IAM principal to attack and confirms one is compiled into public code at all. Paired with its secret, the permissions ride along unchanged: if that principal can write to S3, the objects your site serves can be replaced with theirs.
Learn more about AWS access key ID →Private key
Seven literal PEM header lines: -----BEGIN PRIVATE KEY-----, its RSA, EC, OPENSSH, DSA and ENCRYPTED forms, and -----BEGIN PGP PRIVATE KEY BLOCK-----Alone among these patterns it has no length bound, because what it matches is a header line rather than a token: the armoured body underneath is never parsed, so the algorithm, the modulus size and the trailing footer are all unread. Any of the seven openings counts once it appears in content the page already fetched. An ENCRYPTED block is reported like the rest, since a passive read cannot tell you whether the passphrase wrapping it would survive an afternoon of offline guessing. Scored critical. Nothing that matched is ever echoed back to you: the finding type is declared without any field capable of carrying the matched text, so the report can name the file and the kind and is structurally unable to name the bytes.
The consequence is whatever that key authenticates. A TLS key lets someone present a server as your domain; an OpenSSH key is a shell wherever its public half was authorised; a PGP block retroactively opens every message anyone encrypted to it.
Learn more about Private key →GitHub token
ghp_, gho_, ghu_, ghs_ or ghr_ then 36 to 255 characters from A-Za-z0-9Five prefixes, matched as gh[pousr]_, so any of the five shapes trips the same rule. The floor is 36 characters and the ceiling 255, and the ceiling is set that far past the floor so a format change cannot silently blind this: after a bounded quantifier, a trailing word boundary matches nothing at all when the real token is longer than the bound, rather than matching part of it, which is why the range-bounded patterns here close with a negative lookahead. The two fixed-length ones, AKIA and AIza, keep an ordinary word boundary: a format with no "up to N" in it has no oversized variant to miss. Scored critical.
Scope decides the damage and repo scope is the usual grant: private source, the workflow files, and every other credential that was committed to those repositories before the team learned not to, all readable by anyone who copies the string out of a public asset.
Learn more about GitHub token →Slack token
xoxb-, xoxa-, xoxp-, xoxr- or xoxs- then 10 to 300 characters from A-Za-z0-9 and hyphenThe one pattern here whose body admits a hyphen, since these tokens are written as dash-separated groups, and it stops at the first character that is neither alphanumeric nor a hyphen rather than at a word boundary that a hyphen would trip. Five prefixes, matched as xox[baprs]-, over a 10 to 300 character body. Scored critical.
A workspace token is an identity inside your team, not merely a data leak: whoever holds it reads the conversations that token can see and posts into them under a name your colleagues already trust, which is a considerably shorter route to a wire transfer than any exploit.
Learn more about Slack token →SendGrid API key
SG. then 16 to 64 characters, a dot, then 16 to 128 characters, all from A-Za-z0-9, underscore and hyphenTwo variable-length segments behind the prefix, separated by a literal dot, both bounded and both required for the token to match at all. That shape is what keeps an abbreviated SG. example in a README or a config sample from raising anything: sixteen characters is the floor on each half, and a placeholder rarely carries that many. Scored critical.
Mail sent with a working key leaves through your account, aligned with the SPF and DKIM records you publish, so it arrives authenticated. The phishing message your customers are least prepared for is the one your own domain vouched for.
Learn more about SendGrid API key →Supabase service-role key
An eyJ-prefixed JWT, three base64url segments of 5 to 2000, 10 to 8000 and 5 to 2000 characters, whose decoded payload carries "role": "service_role"Decided by content rather than by shape, and the only one of the eight that is. Every JWT in the content we read is a candidate; the middle segment is base64url-decoded and parsed, and a finding is raised only where the payload is a JSON object whose role is exactly service_role. Anything that fails to decode, or decodes to an array or a bare value, is dropped in silence. A role of anon is left alone deliberately, because that key is designed to travel to the browser and row-level security rather than secrecy is what holds it back, and so is any role we do not recognise: guessing at those is precisely the false positive this classifier exists to avoid. Scored critical.
This key is defined as the one that goes around row-level security, so the exposure is never scoped to a table or a tenant. Read and write across the entire project, whatever your policies say, until it is rotated in the dashboard.
Learn more about Supabase service-role key →Google API key
AIza then exactly 35 characters from A-Za-z0-9, underscore and hyphenHeld at minor and never raised, which no other credential here is. Restriction is what decides whether one of these matters, restrictions live in the provider console and not in the string, and settling the question would mean asking Google, which we never do: no credential here is checked against the service that issued it. A second suppression runs before the severity even applies: where both authDomain and projectId appear within a 500 character window either side of the candidate, quoted or bare, it is read as a Firebase web configuration and no finding is raised whatsoever, because those fields are published together by design and the key on its own opens nothing that Firebase security rules do not already govern.
An unrestricted key spends your quota. Whoever lifts it out of your JavaScript makes their requests against your project, and the usage lands on your account rather than theirs, so the bill and the rate limit arrive together, driven by traffic that was never yours.
Learn more about Google API key →The all-clear, and what it is scoped to
One of three result strings: "No credentials detected in scanned scripts or page HTML", "No credentials detected in the content we could read. Scan coverage was partial (see Scan coverage)", or "No credentials detected in the page HTML. No script file could be read on this page, so the JavaScript it ships was not checked at all (see Scan coverage)"What gets classified is the body of every same-origin script the page already fetched plus the page HTML itself, which is where a hydration payload such as __NEXT_DATA__, __NUXT__ or a __sveltekit_ state block deposits server data. Which of the three sentences you get is decided before the all-clear is written, not appended to it afterwards: a skipped script or a file past the classifier size limit produces the second, and a page where not one script body could be read, the ordinary shape of a site whose bundles are on a CDN hostname, produces the third. That last case still reports rather than going quiet, because the HTML genuinely was read in full and inline scripts are a real place for a key to sit, but it says in its own sentence that your JavaScript was not part of the answer. The qualification is inside the finding rather than in a footnote under it, so no reading of a green Exposure category credits this scan with having opened a file it never opened.
A tool that prints the same green "no secrets found" whether it read your whole bundle or none of it is teaching you to skim the one line that mattered, and you will skim it on the scan where a bundle was too large to read and the key was in that bundle.
Source map references
//# sourceMappingURL= or the older, deprecated //@ sourceMappingURL= in the body of a same-origin scriptA deliberately narrow substring test, run independently of the credential patterns and never counted as one of them. We look for the comment; we do not request the .map file it names and we do not read what is inside it. Scored major, with one finding listing every file that carries a reference and each URL cut back to its origin and path, since a query string can itself carry a credential. A reference pointing at a map that was never actually deployed still counts here, because ruling that out would mean issuing a request, and this check issues none.
A published source map hands back the original unminified source: internal path names, developer comments, dead code paths, the shape of an API you never documented, and any commented-out credential that minification would otherwise have buried.
Learn more about Source map references →Scan coverage
The count and the URLs of scripts we could not fully read, the origins that served them, and any file that ran past the classifier limit and was read only in partFive things can open a gap and the result names every one it hit. A single script over 524,288 bytes (512 KB) is skipped. Scripts are read against a running budget of 3,145,728 bytes (3 MB) and skipped once it is spent. A file longer than 2,000,000 characters is classified up to that point and reported as partially checked. A body served from another origin is recorded with the origin that served it, and a read still in flight when the drain window closed is recorded as timed out. This is always an informational pass and never a fail, deliberately, because it says nothing about whether a credential is present, only about how much we managed to look at.
The file most likely to exceed the per-file cap is your largest application bundle, which is also the file most likely to have a key compiled into it, so a skip recorded here is not a rounding error against the all-clear above: it is the all-clear being silent about the most probable place.
What we can't see
- Any file your page did not already request. This check reads response bodies the browser had fetched anyway and issues no request of its own, so a .env, a .git/config, a stray database dump, or an admin bundle no visitor loads is never tested. Tools sold for this job routinely guess at those paths, and a clean result here is not evidence that guessing would come up empty.
- The contents of scripts served from another origin. A response body is read only when its URL origin matches the page, so a bundle on a separate CDN hostname is never classified for credentials. The coverage result does now name every origin it could not read, which is what lets you tell your own CDN from an analytics vendor and see exactly how much of your JavaScript the all-clear above is silent about.
- The contents of a script still arriving when the drain window closed. Reads in flight get 5,000 ms after the page finishes loading and then we stop waiting. That file is reported as timed out and counted among what we could not read, so it is visible rather than lost, but nothing in it was ever classified.
- A credential that does not resemble one of the eight patterns. There is no entropy heuristic here and no guessing: a database password in a config object, an internal signing secret, a bare hex key from a smaller provider, or a bearer token with no distinguishing prefix all pass through unremarked.
- Whether a key we flagged still works. We never call the issuing provider, so a Stripe key you rotated last week still matches sk_live_ and still gets reported. The same silence costs us in the other direction on the Google pattern, where a key locked to your referrer and a wide-open one are indistinguishable without asking Google, which is precisely why that one is capped at minor instead of critical.
- Bundles that only load somewhere else: a route we did not visit, a component behind a login, an admin area, or a chunk fetched only after a click. Code splitting means the URL you hand us usually loads a fraction of the JavaScript your site ships.
- Whether a key is gone from everywhere, as against gone from this build. We read what is live right now, so a credential pulled out of the current bundle reads clean here while it is still sitting in git history, in a previous deploy, and in whatever a CDN has cached. Rotating it at the provider is what ends the exposure; deleting the line is not.
Ready to see where you stand?
Scan your site and get your Engineering Score with a prioritized roadmap in under a minute, no signup required.
EngineeringScore