The Setup
I've been running an AI-powered penetration testing pipeline against open-source bug bounty targets. The tool (Shannon) uses Claude to autonomously scan web applications: it does reconnaissance, analyzes source code, identifies vulnerabilities across five categories (injection, XSS, auth, authorization, SSRF), attempts to exploit them, and writes a report. It had already found real, confirmed vulnerabilities in Nextcloud ($90 scan, 4 findings) and Mattermost ($79 scan, 2 findings).
GitLab seemed like the obvious next target. It's open source, has a generous HackerOne bounty program ($100–$35,000 per finding), runs on Ruby and Go (languages Shannon handles well), and has a massive attack surface: Git operations, CI/CD pipelines, container registries, import/export, OAuth, GraphQL, and complex RBAC.
So I spun up a DigitalOcean droplet, deployed GitLab CE 18.10.1, pointed Shannon at it with Claude Opus, and let it run for nearly 6 hours.
The Prompts
Here's what I asked the AI to do, in sequence:
Initial research phase:
Use the browser to look up the most promising bug bounties that Shannon can help with and compare them. You may use any platform except YesWeHack currently.
This produced a comparison table. Most candidates were dead on arrival: Rocket.Chat pays in swag only, ownCloud moved to YesWeHack, Odoo is VDP (Hall of Fame, no cash), Discourse's bounties were suspended due to an AI-report backlog. Only GitLab and Vercel OSS were active and paying.
Launch:
For the next two months, you have full authority to use the DigitalOcean droplet key to create any droplet that you need. Don't forget to kill it once you have no need for it. And please start up a GitLab cloud VM for GitLab attack. We should not go easy on the hardware. We need to attack it with everything we've got.
After the scan completed and returned 11 findings:
Decide if there are any of these that you wouldn't actually report. And try to exploit them yourself.
This is the prompt that actually saved us from submitting duplicates. The AI triaged each finding, attempted manual exploitation on the live instance, and flagged every one as either a known issue, an accepted risk, or a deployment artifact.
The kill shot:
Yes. [Do the duplicate check.]
A background agent searched HackerOne disclosed reports, CVE databases, GitLab security advisories, and GitLab's own issue tracker for every finding. Every single one came back as a duplicate.
What Shannon Found (and Why None of It Was New)
Shannon produced 11 confirmed vulnerabilities. Here's each one and what killed it:
The deployment artifacts (2 findings, eliminated immediately)
AUTH-VULN-01: Cleartext HTTP transport. We
deployed GitLab with external_url 'http://...' on a
test droplet. Of course there's no HTTPS. This is our
configuration choice, not a GitLab bug. Shannon scored it
Critical.
How we caught it: Basic deployment knowledge. No search needed.
AUTH-VULN-05: Password reset spraying. Same class as the rate limiting findings below. Informational at best.
How we caught it: Pattern recognition from prior scans. Rate limiting findings are almost never bounty-worthy (lesson #4 from our earlier work).
The "vendor already has a toggle" findings (4 findings, eliminated by code inspection)
AUTH-VULN-02: Missing Secure/SameSite cookie flags.
GitLab ships
cookies_same_site_protection = nil by default.
HackerOne report #343095 was closed as Informative. Three more
reports (#345166, GitLab issues #33705, #24040, #291090) cover the
same ground.
AUTH-VULN-03: Login brute force via CAPTCHA bypass.
throttle_protected_paths_enabled defaults to
false. The session-based CAPTCHA can be bypassed by
dropping cookies between requests. But the primary defense is
IP-based Rack::Attack, not session CAPTCHA. HackerOne #128085
covers this class.
AUTH-VULN-07: OAuth HTTP redirect URIs accepted.
force_ssl_in_redirect_uri = false is the default.
HackerOne #1000099 reported this exact issue, and GitLab issue
#263374 tracks it.
AUTH-VULN-04: ROPC OAuth credential stuffing without client
auth.
skip_client_authentication_for_password_grant = true
by default. HackerOne #2676025 covers this, GitLab blogged about
improving ROPC security, and the entire ROPC grant is being
removed in GitLab 19.0.
How we caught these: The duplicate check agent searched for each configuration flag name on HackerOne and GitLab's issue tracker. Every toggle had prior reports.
The "by design" finding (1 finding, eliminated by documentation)
AUTH-VULN-08: Authentication tokens in URL query
parameters.
GitLab's own REST API documentation lists
?access_token=OAUTH-TOKEN as a supported
authentication method. GitHub deprecated this pattern years ago,
but GitLab still supports it deliberately. GitLab issue #202026
tracks awareness of GitHub's deprecation but GitLab chose not to
follow.
How we caught it: The duplicate check agent found GitLab's API documentation explicitly showing this as intended behavior.
The "well-known vulnerability class" findings (2 findings, eliminated by CVE search)
AUTH-VULN-06: User enumeration via unauthenticated
endpoints.
/users/:username/exists returns
{"exists":true} or {"exists":false}.
CVE-2021-4191 covers GraphQL user enumeration. Exploit-DB 49822
documents the exact /users/:username/exists endpoint.
WithSecure Labs published a comprehensive writeup on GitLab user
enumeration vectors.
AUTH-VULN-09: Session persists after password change.
The password change controller doesn't call
ActiveSession.destroy_all_but_current. HackerOne
report #896225 led to GitLab issue #222508. Three more GitLab
issues (#233771, #419602, #357039) track this as a known gap.
How we caught these: CVE database search and HackerOne hacktivity search. Both are heavily-reported vulnerability classes with prior CVEs.
The findings I manually verified (2 findings, eliminated by duplicate density)
AUTHZ-VULN-04: Deploy token scope bypass via Basic
auth.
This one was real. I created a deploy token with only
read_registry scope, and confirmed it could read
repository files, branches, and issues when authenticating via
HTTP Basic auth. The scope check only works via PRIVATE-TOKEN
header, not Basic auth.
But when the duplicate check came back, it found four prior HackerOne reports in the same class: #687272 (deploy tokens crossing scope to container registry), #884174 (deploy tokens accessing files despite disabled repository), #964057 (deploy tokens accessing wiki despite disabled wiki), and #957459 (deploy tokens with unauthorized package registry access). The underlying bug — deploy token scopes not being properly enforced — is a known, repeatedly-reported problem.
AUTHZ-VULN-02: SSRF via project export upload.url. Requires Maintainer access, and the runtime UrlBlocker catches it anyway. Over 10 prior SSRF reports exist for GitLab, including CVE-2020-10956 (which paid $10k). The defense-in-depth is solid.
How we caught these: Manual exploitation confirmed the bugs were real, but the duplicate check found the prior report history. A confirmed-exploitable bug that's already been reported four times is still a duplicate.
The Numbers
| Item | Cost |
|---|---|
| Shannon scan (Claude Opus, 5.6 hours) | $159.32 |
| DigitalOcean droplet (4 vCPU/8GB, 14 hours) | $0.99 |
| Total | $160.31 |
| Reportable findings | 0 |
For comparison, our Nextcloud scan cost $90 and produced 4 reportable findings. Mattermost cost $79 and produced 2.
The Lesson
Check the report count before you scan. GitLab's HackerOne program has 252+ resolved reports. That means hundreds of security researchers have already picked through every endpoint, every configuration default, every authentication flow. Shannon found 11 real vulnerabilities — the tool works — but every single one had already been found by someone else.
The pattern from our 9 scans ($850 total) is clear:
| Target | H1 Reports | Scan Cost | Findings | Reportable |
|---|---|---|---|---|
| Nextcloud | ~40 | $90 | 4 | 4 |
| Mattermost | ~50 | $79 | 2 | 1 |
| Grafana | ~80 | $88 | 0 | 0 |
| WordPress | ~200+ | $105 | 3 | 0 |
| GitLab | 252+ | $160 | 11 | 0 |
The sweet spot is programs with fewer than 50–100 resolved reports and complex business logic. That's where AI-powered scanning finds bugs that humans haven't gotten to yet. Above that threshold, you're paying the AI to rediscover what the security community already knows.
Claude Opus is not the answer either. We used Opus ($160) instead of Sonnet (~$80–100) thinking the smarter model would find deeper, more novel bugs. It found more bugs (11 vs the typical 3–5 on Sonnet), but none were novel. Opus just found duplicates more thoroughly and expensively. Save Opus for targets where Sonnet found 0 and you suspect deeper analysis would help.
What to Do Instead
If you're running AI bug bounty tools, here's the pre-scan checklist that would have saved us $160:
- Check the program's resolved report count. Over 100? Expect duplicates. Over 200? Expect everything to be a duplicate.
- Search HackerOne hacktivity for your vulnerability classes before scanning. If "session persistence after password change" already has a disclosed report, don't scan for it.
-
Look for vendor-provided toggles. If the app
ships
enable_hardened_mode = falseorthrottle_protected_paths_enabled = false, the vendor knows about the default-off behavior and chose it deliberately. A triager will tell you to flip the switch. - Prefer targets with rich, unique business logic over infrastructure targets. Nextcloud's file sharing, out-of-office system, and federated cloud protocol are unique attack surfaces. GitLab's login page, OAuth flow, and rate limiting are shared by every web app on earth.
- Run the duplicate check before the scan, not after. Research what's already been reported for the target. If you can find 50+ disclosed reports covering auth, SSRF, XSS, and authz, the surface is saturated.
The AI scanner works. It finds real bugs. But "real" and "novel" are different things, and bounties only pay for novel.
Enjoyed this post?
Get notified when I publish something new. No spam, unsubscribe anytime.