Most bug bounty hunters lose before they start because they all fish the same hole. They clone a popular project, point a scanner at it, and grep the same patterns everyone has grepped for three years. By the time you arrive, every static finding worth having is fixed, reported, or in someone else's draft. The codebase is trampled.
So I stopped hunting old code. I hunt code that did not exist last week.
This is my single most useful lens for bug-hunting AI/ML infrastructure on platforms like huntr: recency of code. AI infra — RAG engines, agent frameworks, vector pipelines, model servers — ships absurdly fast, multiple releases a month. Every release adds new HTTP routes, new file parsers, new external connectors, new template rendering — new ways to feed untrusted input into a system that was never threat-modeled as an attack surface. That code has been reviewed by exactly one population: the maintainers who wrote it, in a hurry. The prior hunter sweep never touched it because it wasn't there during the sweep.
That gap is the entire opportunity.
Diff release-to-release, not the whole repo
I do not read the project. I read the delta. The workflow is boring on purpose:
# pin the two boundaries you care about
git fetch --tags
git log --oneline v1.2.0..v1.3.0 # what landed since the last cut
git diff v1.2.0..v1.3.0 -- '*.py' # the actual new attack surface
I throw away everything that isn't reachable from untrusted input. Refactors, tests, doc strings, no-op dependency bumps — gone. What survives is the short list of new sinks and new sources: a new endpoint, a new upload handler, a new "fetch this URL for me" feature, a new prompt template that interpolates user data, a new export/import path.
Reading a diff is also how you reconstruct intent. A release note that says "added importing knowledge from a remote source" is a flashing arrow toward server-side request forgery. "Added a customizable template for responses" points at template injection. The changelog tells you where the developers added power; power added quickly is power added carelessly.
Triage the untrusted-input surface, in order
Once I have the new code, I triage every new entry point against a fixed checklist. I am not trying to be clever — I am trying to be complete, because completeness is what beats the crowd.
- SSRF — anything that takes a user-supplied URL/host and makes the server fetch it: "ingest from this link," "load this remote dataset," webhook callbacks, image fetchers. Look for a request built from input with no allowlist and no block on internal ranges.
-
Authz / IDOR — new endpoints that take an object ID but check authentication without checking ownership. Fast teams add the route and the
@login_requireddecorator and forget the "does this user own resource N" step. - Injection (SQL / NoSQL / command) — new query builders that concatenate input, new shell-outs to convert a document or call a model binary.
- SSTI — template engines fed user-controlled strings. Common in LLM tooling, where "prompt templates" and "report templates" look innocent and get rendered server-side.
-
Path traversal — new file read/write/export features that join a base directory with a user-supplied name. The classic
../../etc/...lives wherever someone added "download your file." - Insecure deserialization — new code that loads pickles, YAML, or model artifacts from a path the user influences. ML land is full of this, since model files and configs get deserialized as a matter of course.
Here is the generic shape I look for, not any specific bug:
# new in this release — fetches a user-named resource
@router.post("/v2/resource/import")
def import_resource(source: str): # SOURCE: untrusted
data = http_client.get(source) # SINK: SSRF, no allowlist
path = os.path.join(STORAGE_DIR, source_name) # SINK: traversal
return loader.load(path) # SINK: deserialization?
Three potential bug classes in five lines of brand-new code. That is what a fresh diff looks like when the team is moving fast.
Fan out the audit, then refute by default
This is where AI earns its keep — and where most people misuse it. Pointing one model at a diff and asking "any vulns?" gets you a confident pile of garbage. False positives are not free on a bounty platform: a stream of bogus reports degrades your reputation, and on platforms that penalize low-quality submissions, it can cost you the account. The account is the asset.
So I run two phases.
Phase 1 — fan-out. I split the new surface across several independent auditor passes, each with a narrow mandate ("only SSRF in these three files," "only authz on these endpoints"). Narrow scope beats one model holding the whole release in its head. Each pass produces candidates, not findings.
Phase 2 — refute by default. Every candidate goes to a separate adversarial verifier whose job is to kill it. The default verdict is "this is not exploitable; prove me wrong." The verifier has to trace a concrete path from an untrusted source to the dangerous sink with no guard in between — the function that receives input, the call chain, and the exact missing check. If it cannot build that chain, the candidate dies. No "looks suspicious." No "could potentially." A finding survives only when an adversary trying to disprove it failed.
This refute-by-default posture is the whole reason the pipeline is safe to point at a real account. The fan-out gives you recall; the adversarial verifier gives you precision. You submit only the small set that survived someone actively trying to throw it away.
The discipline of walking away
Here is the part nobody writes about: most diffs are clean, and you have to be willing to get nothing.
You pin two tags, pull the delta, run the whole pipeline, and the honest answer is "the new code is fine." The temptation is enormous — you spent the time, you want a return, so you start stretching a weak candidate into a report. That is exactly how you train a platform to distrust you. The expected value of a stretched report is negative: a small chance of a payout, a real chance of a rejection that follows your handle around.
Walking away from a clean diff is not a failure of the method. It is the method. The edge of hunting fresh code is that you check many small deltas cheaply and only engage when one actually breaks. Volume of looks, not volume of reports.
One reason this post is abstract: I am running this exact technique right now against a popular fast-shipping RAG engine, with findings that are not yet reported and not yet fixed. So there are zero specifics here — no component, no version, no payload. The point is the process, and the process is fully transportable: diff the new release, map the new untrusted-input surface, fan out narrow audits, refute everything by default, submit the survivors, and walk away from the clean ones.
Stop fishing where everyone fishes. Go where the code is new.
Top comments (0)