Dormant GitHub accounts: how ghost profiles quietly map corporate orgs
Datadog Security Labs is tracking several overlapping campaigns that walk corporate GitHub organizations one API call at a time. The tooling is unremarkable. What makes it hard to spot is the identity behind the traffic: accounts created years ago, left idle, then switched on to enumerate members, repositories, and contribution graphs. This is a reconnaissance bug class more than a memory-corruption one, and the fix is detection, not a patch.
What the campaign actually does
The activity is enumeration, not exploitation. Operators drive automated scraping tooling against the GitHub API to build a map of a target organization: its public repositories, its members, who those members follow, and which projects each account touches. Individually every request is legitimate. Read together they reconstruct a company's internal structure, its team boundaries, and the developers most worth phishing later. In a smaller number of cases the same tooling escalated from public-data collection to cloning private repositories using credentials it had already harvested.
Because a large part of GitHub's API surface answers without authentication, a lot of this map can be assembled with no login at all. The unauthenticated tier is rate-limited but generous, and the data it returns is exactly what a threat actor needs for the first phase of an intrusion: names, project affinities, and the shape of the org chart.
Why dormant accounts are the trick
The reconnaissance itself is old news. The interesting part is how the operators stay under the noise floor. Datadog describes the widest band of traffic coming from networks of ghost accounts: profiles registered two to five years ago, left dormant, then activated to spray API queries across many organizations.
An account with a multi-year history reads as more legitimate than one registered the same week it starts scraping. the core of the evasion
Age is the disguise. Most naive abuse heuristics weight account age heavily: a week-old account hitting a hundred orgs looks like a bot, a four-year-old account doing the same looks like a long-time user. The campaign weaponizes that assumption. Alongside the ghost accounts, the operators fold in dozens of legitimate accounts whose personal access tokens (PATs) or OAuth tokens were exposed or stolen, so some of the traffic comes from genuinely trusted identities. Rotating across more than fifty dormant profiles plus the compromised set also spreads the load under per-account rate limits, so no single identity trips a throttling alarm.
The bug class: identity is not intent
Frame this the way you would frame any detection problem. The underlying weakness is that account reputation is being used as a proxy for good intent, and the two are not the same thing. A dormant account is not a trustworthy account; it is an account with no recent behavior to judge. When your baseline is "old and quiet equals safe," an adversary who can buy or age quiet accounts inherits your trust for free.
This is the same shape as credential-stuffing that rides real accounts, or residential-proxy traffic that borrows real home IPs to look human. The defensive lesson transfers: reputation signals decay, and any signal an attacker can cheaply manufacture stops being a signal. What does not decay is behavior. A four-year-old account that has never touched your org and suddenly enumerates every member in an afternoon is anomalous no matter how old it is.
Detecting it on your own org
If you run a GitHub organization, the enterprise and organization audit logs are the primary evidence source. You are not looking for a known-bad indicator; you are looking for access patterns that do not match how humans use the API. Practitioner starting points:
- Breadth over depth. A single identity reading many members, followers, and repos in a short window, touching areas it has no working relationship with, is enumeration, not development.
- First-contact anomalies. An account with a long history but no prior interaction with your org that appears and immediately walks the member list is the ghost-account pattern.
- User-agent and token oddities. Custom or oddly generic user agents, or a PAT being used from new geographies and ASNs, are worth pivoting on.
- Private-clone spikes. Any clone of a private repository by a token that has never cloned it before is the escalation signal, and the one that actually costs you.
# pull org audit events and rank actors by breadth of read access gh api "/orgs/$ORG/audit-log?phrase=action:git.clone&per_page=100" \ --paginate | jq -r '.[] | [.actor, .repo, .created_at] | @tsv' \ | sort | uniq -c | sort -rn | head -20 # actors touching many distinct repos in a narrow window rise to the top
Reducing the attack surface
You cannot stop the internet from reading your public data, and you should not try. What you can do is shrink the private blast radius and make the leaked-token path narrower.
- Kill long-lived PATs. Prefer fine-grained tokens scoped to specific repositories with short expiries. A stolen token that expires in seven days and can read one repo is a far smaller prize than a classic PAT with full
reposcope. - Enforce SSO and IP allow lists on the organization so a stolen token alone is not enough to reach private data.
- Turn on secret scanning with push protection so credentials never reach a public repo in the first place, which is where most of the compromised tokens in campaigns like this come from.
- Audit third-party OAuth apps and revoke ones nobody remembers installing. Every authorized app is a token an attacker would love to inherit.
The broader ecosystem is moving the same direction. GitHub's release of npm 12 disables install scripts by default and deprecates the granular access tokens that used to sidestep two-factor prompts, both aimed at cutting the number of long-lived, over-scoped credentials floating around developer machines. Fewer standing tokens means fewer that can be quietly folded into a scraping fleet.
Reproducing the shape in a lab
You do not need anyone's real org to understand this. Create a throwaway organization, add a couple of test accounts, and script read-only enumeration against your own targets with a token you control. Watch how the audit log records it, then write a query that separates your enumeration run from normal activity. The goal is not the scraper; it is building the detection that would have caught it. An adversary's whole strategy here is to look normal, so your whole job is to define, precisely and in your own environment, what normal is not.
Sources
- The Hacker News. "Dormant GitHub Accounts Help Attackers Blend In While Mapping Corporate Orgs." Read the report
- Datadog Security Labs. "Coordinated GitHub API enumeration and access token abuse." Read the analysis
- The Hacker News. "npm 12 Disables Install Scripts by Default to Reduce Supply Chain Risk." Read the report
- GitHub Docs. "Managing dormant users." Read the documentation