DEV Community

Trix Cyrus
Trix Cyrus

Posted on

I built LLMHunter to hunt exposed LLM API keys

Author: Trix Cyrus

[🔹 Try My] Waymap Pentesting Tool

[🔹 Follow] TrixSec GitHub

[🔹 Join] TrixSec Telegram

[🔹 Tool Link] LLMHunter


LLM applications are everywhere now, and so are API keys sitting inside frontend code, JavaScript bundles, source maps, and configuration files.

So I built LLMHunter.

LLMHunter is a Python CLI tool for discovering and validating potentially exposed LLM API keys across web applications and client-side assets.

What can it find?

It currently supports:

  • Google Gemini
  • OpenAI
  • Anthropic Claude
  • NVIDIA NIM

The interesting part isn't just finding obvious strings like sk-....

LLMHunter also looks through:

  • Inline and external JavaScript
  • JSON manifests
  • Webpack chunks
  • JavaScript source maps
  • Wayback Machine snapshots
  • Obfuscated strings
  • Split string concatenations
  • Template literals
  • Base64 and hex encoded strings
  • Reversed payloads

Once a candidate key is found, LLMHunter can validate it against the relevant provider and gather additional information depending on the provider.

It can also check for things like referrer/origin restrictions and generate evidence that can be used during an authorized security assessment.

A few usage examples

Scan a target:

llmhunter example.com
Enter fullscreen mode Exit fullscreen mode

Scan multiple targets:

llmhunter -f targets.txt --concurrency 20
Enter fullscreen mode Exit fullscreen mode

Validate keys directly:

llmhunter --key-file keys.txt
Enter fullscreen mode Exit fullscreen mode

Filter by provider:

llmhunter example.com --provider openai --provider anthropic
Enter fullscreen mode Exit fullscreen mode

Generate a JSON report:

llmhunter example.com -o report.json --evidence
Enter fullscreen mode Exit fullscreen mode

I also added an offline testing sandbox with mock web targets and mock LLM providers, so the project can be tested without hitting real services.

Why I built it

A lot of credential hunting tools are really good at finding obvious patterns.

But modern web applications don't always expose credentials as a clean:

sk-xxxxxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

Sometimes they're split across strings, hidden inside bundles, encoded, buried in source maps, or only present in older versions of a site.

I wanted to experiment with building something that follows those trails instead of stopping at the first regex match.

There's still a lot I want to improve, but this is a good starting point.

GitHub: https://github.com/TrixSec/llmhunter

LLMHunter is intended for authorized security testing, penetration testing, and defensive research. Don't scan targets you don't have permission to test.

~TrixSec

Top comments (7)

Collapse
 
analista_83 profile image
Sammi De Blas •

From the defender side, this is exactly the kind of tool I would run against our own public assets before someone else does. Two things that would be interesting to see next: (1) source-map exposure as root cause - if a key lives in a .map file, rotating the key does not close the leak channel and the same bundle may expose other secrets later; (2) a 'was it ever valid' angle on Wayback hits - old snapshots with keys that were rotated afterwards are gold for incident reconstruction ('when did this key exist publicly?'). Question: when you validate candidates against the provider, do you also capture the key's scope/permissions (read-only vs billing-enabled)? That is what separates a 'leaked key' finding from an actually exploitable one.

Collapse
 
trixsec profile image
Trix Cyrus •

Yeah, that's a really good point. Right now LLMHunter validates whether the key is active and gathers provider-specific intelligence, but it doesn't fully map the key's effective permissions/scope yet. That's definitely something I want to add.

The source-map angle is also interesting. I'm already chasing .js.map files, but treating the source map as the root cause and correlating everything exposed through the same bundle would make the findings much more useful. Same with Wayback: tracking when a key first appeared, whether it was valid at that point, and when it stopped working could turn it into a pretty useful incident-reconstruction feature.

Honestly, those are both going on the roadmap. Thanks for the ideas!

Collapse
 
analista_83 profile image
Sammi De Blas •

Scoping the key before treating it as exposed is the right ordering - a read-only key bound to one project changes the severity entirely. For reconstruction, the Wayback angle gives you a validity window per key; correlating that window with the bundle's source map would bound which endpoints were reachable while the key was live.

Collapse
 
analista_83 profile image
Sammi De Blas •

One cheap way to get the scope mapping without burning tokens: a GET on the provider's model-list endpoint answers both validity and access in one call - an OpenAI-compatible /v1/models with the candidate key returns 401 if dead, 200 with the exact model set if valid, and a restricted list tells you what the key can actually reach. For the Wayback window, the CDX index gives you first/last snapshot timestamps per URL, which bounds the exposure period without re-fetching every snapshot.

Collapse
 
trixsec profile image
Trix Cyrus •

That's a great approach. Using the model-list endpoint to map accessible models alongside key validity could make validation much more informative without unnecessary API calls. I'd still distinguish model access from effective permissions, since a restricted model list doesn't necessarily reveal billing privileges or the full scope of a key.

Combining CDX timestamps with source-map analysis to build an exposure timeline is also a strong idea. I'll look into integrating both into LLMHunter. Thanks for the detailed suggestions!

Collapse
 
analista_83 profile image
Sammi De Blas •

Agreed on the distinction - and I would formalize it as tiers rather than a single scope claim: the model-list endpoint is a lower bound on what the key can do, not an inventory. So classify each finding as (a) valid + listable models = confirmed capabilities, (b) valid + restricted or empty list = capability unverified, flag for manual review, (c) invalid. The report then states confirmed capabilities plus an explicit unverified remainder instead of asserting full scope - which is exactly the gap you are pointing at, made visible in the output rather than assumed away.

One calibration note on the exposure timeline: CDX timestamps mark when a page was crawled, not when the key was exposed or rotated. First sighting is an upper bound on exposure start, and rotation detection needs key-string matching across snapshots - the same string reappearing later means still valid. And for source maps: verify reachability before scoring severity. A key found only via a .map that 404s in production is a different finding from one served to the browser.

Collapse
 
analista_83 profile image
Sammi De Blas •

Fair distinction - I would treat the model list as a lower bound on access and probe one admin or billing endpoint to separate scoped-down from dead, since a 403 there means the key is live but restricted. For the timeline, joining CDX first/last timestamps with the bundle hash from the source map gives an exposure window per key rather than per URL, which is what incident reconstruction actually needs.