secrets-in-git-history

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Secrets in git history

Git历史中的机密信息

A repository is three intelligence products: a list of humans with real email addresses, a map of the organisation's infrastructure, and a credential store nobody meant to publish. The costly mistake is scanning the working tree. Secrets deleted from HEAD stay in history forever, and commits deleted from a branch stay reachable through the fork network — scan only what is checked out and you are searching the one place the secret was definitely removed from.
一个代码仓库包含三类情报:带有真实邮箱地址的人员列表、组织的基础设施映射,以及无人有意公开的凭证存储库。最容易犯的代价高昂的错误就是只扫描工作目录。从HEAD中删除的机密会永远留在历史记录中,从分支中删除的提交仍可通过分支网络访问——仅扫描已检出的内容,相当于在一个机密肯定已被移除的地方搜索。

What you're holding, and where to start

你手中的资源及入手方向

You haveStart withWhy
An organisation namePublic org members, then their personal reposCorporate repos are reviewed; personal ones are not
A developer's name or handleCommit emails across their reposYields an email nothing else gives you
A commit emailReverse-search across hosts; resolve any noreply IDLinks accounts across orgs and platforms
A suspected credential leakBoth scanners over a mirror clone, all refsHEAD-only scanning misses the point entirely
A specific string — hostname, key prefix
git log -S
pickaxe across all refs
Tells you when it entered and when it left
A live domainCheck for an exposed
.git
— see the legal note
Full history from a web server, if authorized
Nothing but a company websiteCode search the org's internal domain namesFinds repos with no obvious link to the org
你拥有的信息入手点原因
组织名称公开组织成员,再到他们的个人仓库企业仓库会经过审核;个人仓库则不会
开发者姓名或用户名跨仓库提取提交邮箱能获取其他渠道无法提供的邮箱
提交邮箱跨平台反向搜索;解析noreply ID关联不同组织和平台的账号
疑似凭证泄露在镜像克隆仓库上运行两款扫描工具,覆盖所有引用仅扫描HEAD完全偏离了检测核心
特定字符串——主机名、密钥前缀跨所有引用执行
git log -S
pickaxe命令
能告诉你该字符串何时加入、何时移除
活跃域名检查是否存在暴露的
.git
目录——参见法律说明
若获得授权,可从Web服务器获取完整历史
仅知道公司官网代码搜索组织的内部域名找到与组织无明显关联的仓库

The commit email leak

提交邮箱泄露问题

Git records an author identity on every commit, self-asserted rather than verified. That cuts both ways: emails are trivially forged, and they are also the most reliable public link between a code-host identity and a real mailbox.
bash
git clone --mirror https://github.com/example-org/example-repo.git repo.git
git --git-dir=repo.git log --all --format='%an <%ae>' | sort -u
GitHub's web UI does not display commit emails, which leads people to assume they are hidden. They are not. Append
.patch
to any commit URL for a mail-format patch whose
From:
header carries the author name and email; the API returns the same under
commit.author.email
.
bash
curl -s https://github.com/example-org/example-repo/commit/<sha>.patch | head -5
curl -s https://api.github.com/repos/example-org/example-repo/commits/<sha> | jq .commit.author
The noreply pattern is the part worth understanding properly. Users with email privacy enabled commit as
username@users.noreply.github.com
, or in the newer form
12345678+username@users.noreply.github.com
. That second form leaks the account's numeric user ID, which is immutable and survives every rename the account ever makes, so an old commit resolves to whoever holds it today:
bash
curl -s https://api.github.com/user/12345678 | jq '.login, .created_at'
That defeats a rename: an old commit under a long-abandoned handle points at the person's current identity. The complementary failure on the subject's side is enabling email privacy after years of committing — every commit before that date carries the real address, so pull the author list across all history.
Git会在每个提交上记录作者身份,该身份由用户自行声明而非经过验证。这有两面性:邮箱可以轻易伪造,但同时也是代码托管平台身份与真实邮箱之间最可靠的公开关联。
bash
git clone --mirror https://github.com/example-org/example-repo.git repo.git
git --git-dir=repo.git log --all --format='%an <%ae>' | sort -u
GitHub的Web界面不会显示提交邮箱,这让人们误以为邮箱是隐藏的。实际上并非如此。在任意提交URL后追加
.patch
,即可获取邮件格式的补丁文件,其
From:
头包含作者姓名和邮箱;API也会在
commit.author.email
字段下返回相同信息。
bash
curl -s https://github.com/example-org/example-repo/commit/<sha>.patch | head -5
curl -s https://api.github.com/repos/example-org/example-repo/commits/<sha> | jq .commit.author
noreply模式值得深入理解。开启邮箱隐私保护的用户提交代码时,邮箱格式为
username@users.noreply.github.com
,或更新的格式
12345678+username@users.noreply.github.com
。第二种格式会泄露账号的
数字用户ID
,该ID是不可变的,无论账号多少次更名都不会改变,因此旧提交可以关联到当前持有该ID的用户:
bash
curl -s https://api.github.com/user/12345678 | jq '.login, .created_at'
这能破解账号更名的隐藏:使用早已弃用的用户名的旧提交,仍能指向用户当前的身份。而用户这边的常见失误是,在多年提交后才开启邮箱隐私保护——该日期之前的所有提交都带有真实邮箱,因此需要提取所有历史记录中的作者列表。

Enumerating an organisation as a social graph

将组织枚举为社交图谱

Corporate repositories are reviewed. Personal ones are not, and developers put the same infrastructure in both. The pivot is org to human to personal account.
Public org membership is opt-in and therefore incomplete, so triangulate:
https://api.github.com/orgs/<org>/public_members
gives the members who chose to be visible, while the author list across every org repository gives everyone who actually committed, including those who never went public and those who left.
From each human, pull repos, gists, followers, following, starred and watched repositories. Stars and follows are the underused ones: nobody curates them for privacy, and they map interests, employers, side projects and professional relationships more honestly than any profile field. A cluster of developers all starring the same obscure internal-looking tool are colleagues.
Public activity events cover a short recent window, but the public event stream is also published as a historical archive dataset, extending the analysis back years. Commit timestamps in aggregate give working hours and a timezone.
企业仓库会经过审核,个人仓库则不会,而开发者会在两类仓库中使用相同的基础设施。核心思路是从组织转向个人,再到个人账号。
公开组织成员身份是可选的,因此信息并不完整,需要交叉验证:
https://api.github.com/orgs/<org>/public_members
返回选择公开身份的成员,而遍历组织所有仓库的作者列表则能获取所有实际提交过代码的人员,包括从未公开身份的成员和已离职的成员。
针对每个人员,提取其仓库、Gist、关注者、关注对象、标星和订阅的仓库。标星和关注是被低估的信息源:没人会为了隐私刻意整理这些内容,它们能比任何个人资料字段更真实地反映兴趣、雇主、副业和职业关系。如果一群开发者都标星了同一个小众的内部工具,那他们很可能是同事。
公开活动事件仅覆盖近期的短窗口,但公开事件流也会作为历史归档数据集发布,可将分析回溯到多年前。提交时间戳的汇总信息能反映工作时间和时区。

Code search and its real limits

代码搜索及其实际局限性

Host code search finds strings across public repositories. Search what only this organisation would write: internal hostnames, private package names, cloud account IDs, the email domain. The company name finds marketing copy; the internal domain finds infrastructure.
The limits matter more than the syntax, because each is a silent gap. Code search covers the default branch only — no history, no other branches — so the deleted secret is invisible to it by construction. Not every repository is indexed: very large ones, and forks with no independent activity, are commonly excluded. Authentication is generally required and rate limits are tight. And matching is token-based, so a substring inside a longer token may not match the way you expect.
Treat code search as a discovery tool that tells you which repositories to clone. The actual work happens locally.
代码托管平台的代码搜索功能可在公共仓库中查找字符串。搜索只有该组织才会使用的内容:内部主机名、私有包名称、云账号ID、邮箱域名。搜索公司名称只会找到营销文案;而搜索内部域名则能找到基础设施相关内容。
其局限性比语法更重要,因为每个局限性都会造成隐性的检测盲区。代码搜索仅覆盖默认分支——不包含历史记录和其他分支,因此已删除的机密从本质上就无法被检测到。并非所有仓库都会被索引:非常大的仓库,以及没有独立活动的分支通常会被排除。搜索通常需要认证,且速率限制严格。此外,匹配是基于令牌的,因此长令牌中的子字符串可能无法按预期匹配。
应将代码搜索视为发现工具,用于确定需要克隆哪些仓库。实际的检测工作需在本地完成。

Everything that survives deletion

所有删除后仍留存的内容

  • Secrets removed from HEAD remain in history. Deleting a file is a commit, not an erasure; rewriting history does not reach clones, forks or caches; and almost nobody rotates afterwards, because deleting felt like fixing.
  • Commits in a fork network stay reachable by SHA, even after a branch is deleted, a PR is closed unmerged, or the fork is removed. Making a repository private does not retract what reached forks, and force-pushed commits are dangling rather than gone.
  • Gists are unlisted, not private — anyone with the URL reads a "secret" gist, and URLs leak into logs, chats and tickets.
  • CI logs are public on public repositories. Masking of known secret values fails whenever a value is base64-encoded, concatenated, URL-encoded, or printed by a subprocess. Issue and PR attachments are likewise served by opaque URL and stay retrievable after the issue or repository is gone.
  • **从HEAD中移除的机密仍留存于历史记录中。**删除文件只是一次提交,而非彻底擦除;重写历史记录无法覆盖克隆仓库、分支或缓存;而且几乎没人会在删除后轮换凭证,因为他们觉得删除就等于修复了问题。
  • 分支网络中的提交仍可通过SHA访问,即使分支被删除、PR未合并就关闭,或分支被移除。将仓库设为私有并不会撤回已传播到分支的内容,强制推送的提交只是处于悬空状态而非消失。
  • Gist是未公开而非私有——任何知道URL的人都能读取“机密”Gist,且URL会泄露到日志、聊天记录和工单中。
  • **公共仓库的CI日志是公开的。**已知机密值的掩码处理在值被Base64编码、拼接、URL编码或由子进程打印时会失效。Issue和PR附件同样通过不透明URL提供,即使Issue或仓库被删除后仍可检索。

Scanning properly

正确的扫描方式

Mirror-clone, then run both scanners — their detector sets and false-positive profiles differ and neither is a superset. Then pickaxe the strings they have no detector for: internal domains, cloud account IDs, hostnames from config files.
bash
trufflehog git file://./repo.git
gitleaks detect --source=. -v
git log --all --source -S'internal.example.com' --oneline
git log --all --diff-filter=D --name-only --format='%H %aI %an'
One warning about verification. TruffleHog can confirm a candidate is live by calling the provider's API with it. That is an interaction with a third party using someone else's credential, and it writes to their logs. Inside an authorized assessment it is normally expected. Outside one, do not.
Commands in reference/git-archaeology.md; what to grep for and how to triage a hit in reference/high-signal-indicators.md.
先镜像克隆仓库,然后运行两款扫描工具——它们的检测规则和误报特征不同,且彼此并非包含关系。之后,对它们没有检测规则的字符串执行pickaxe分析:内部域名、云账号ID、配置文件中的主机名。
bash
trufflehog git file://./repo.git
gitleaks detect --source=. -v
git log --all --source -S'internal.example.com' --oneline
git log --all --diff-filter=D --name-only --format='%H %aI %an'
**关于验证的警告。**TruffleHog可通过调用服务商的API来确认候选凭证是否有效。这相当于使用他人的凭证与第三方交互,并会写入对方的日志。在获得授权的评估中,这通常是允许的。但在未授权的情况下,请勿执行此操作。
相关命令请参考reference/git-archaeology.md;需搜索的内容及如何分类检测结果请参考reference/high-signal-indicators.md

Exposed .git directories

暴露的.git目录

A deployment that copies a working tree to a web root publishes
.git
with it, and from
.git/config
,
.git/HEAD
and the object store the entire history is reconstructible — source, credentials, author identities, everything.
Observing that such a path exists, through a search index or an existing scan result, is passive. Retrieving it is not. Downloading a
.git
directory is accessing a system and taking data from it, and no misconfiguration constitutes permission. Only inside a written authorization; otherwise, report it.
将工作目录复制到Web根目录的部署方式会同时发布
.git
目录,通过
.git/config
.git/HEAD
和对象存储可重建完整历史记录——包括源代码、凭证、作者身份等所有信息。
通过搜索索引或现有扫描结果发现此类路径的存在属于被动行为。**但检索该目录则不属于被动行为。**下载
.git
目录属于访问系统并获取数据,任何配置错误都不构成访问权限。仅在获得书面授权的情况下才可操作;否则,请上报该问题。

Where this goes wrong

常见误区

  • Author identity is self-asserted. Anyone can commit as anyone; an email in a commit is a claim, not proof of authorship. Signed commits are the exception and the signature is what you verify, not the string.
  • Bots dominate commit counts. CI, dependency bots and merge automation produce huge volumes of commits under synthetic identities. Filter them before concluding anything about people.
  • Rebases and squash merges rewrite authorship. The committer becomes whoever performed the merge, and squashing collapses many authors into one. Absence from the log is not absence from the project.
  • Timezone inference is soft. Laptops travel, offsets get set by hand, CI commits at UTC. Corroboration only.
  • Test fixtures and documentation examples look exactly like credentials, and provider examples propagate into thousands of repos. Search first.
  • Public-by-design keys are not leaks. Publishable and referrer-locked browser keys are meant to ship; reporting one as a breach costs you credibility on the findings that are real.
  • A namesake org or typosquat is not your target, and a history-only finding is often more live than a current one, because removal without rotation is the norm.
  • **作者身份是自行声明的。**任何人都可以冒充他人提交代码;提交中的邮箱只是一种声明,而非作者身份的证明。签名提交是例外情况,此时需要验证的是签名而非字符串。
  • **机器人占据提交数量的主导。**CI、依赖更新机器人和合并自动化工具会以合成身份生成大量提交。在对人员做出结论前,请先过滤掉这些提交。
  • **变基和压缩合并会重写作者信息。**提交者会变成执行合并的人,压缩合并会将多个作者合并为一个。日志中没有某个人的记录并不代表他没有参与项目。
  • **时区推断并不准确。**笔记本电脑会移动,时区偏移可能手动设置,CI提交使用UTC时间。仅可作为佐证。
  • 测试 fixture 和文档示例看起来与凭证完全相同,服务商的示例会传播到数千个仓库中。请先进行搜索验证。
  • **设计为公开的密钥并非泄露。**可发布的、引用锁定的浏览器密钥就是用于发布的;将其报告为泄露会降低你在真实漏洞报告中的可信度。
  • 同名组织或仿冒组织并非你的目标,仅存在于历史记录中的发现往往比当前记录更具时效性,因为删除而不轮换凭证是常态。

Confidence grading

可信度分级

  • Confirmed — an identity link where a noreply numeric ID resolves to the account, or the same commit email appears under multiple accounts already tied to the person. A credential exposure whose format is valid, whose scope the surrounding context establishes, with a commit SHA and date.
  • Probable — a distinctive email or handle recurring across repositories with consistent commit timing, or a credential-shaped string in a real config file outside a test path.
  • Unconfirmed — a single unsigned commit's author string; a common handle; a scanner hit in a fixture or documentation example; a credential of unknown scope.
  • Rejected — a documented placeholder, a publishable key, or an expired temporary credential presented as a live exposure.
Record repository, commit SHA, date and file path for every finding. A SHA is an immutable citation; a line number is not.
  • 已确认——身份关联:noreply数字ID解析到对应账号,或同一提交邮箱出现在多个已关联到该人员的账号下。凭证泄露:格式有效,上下文明确其范围,包含提交SHA和日期。
  • 大概率——独特的邮箱或用户名在多个仓库中重复出现,且提交时间一致;或在测试路径外的真实配置文件中发现类似凭证的字符串。
  • 未确认——单个未签名提交的作者字符串;常见用户名;扫描工具在fixture或文档示例中命中的结果;范围未知的凭证。
  • 已排除——有文档记录的占位符、可发布的密钥,或被当作有效泄露的过期临时凭证。
为每个发现记录仓库、提交SHA、日期和文件路径。SHA是不可变的引用;行号则不是。

Worked example

实操示例

Objective: map the engineering team of a company whose site names only executives.
Code search on the internal domain
svc.example-corp.net
returns three repositories, none carrying the company's name. Mirror-cloning them gives fourteen distinct author emails, eleven corporate and three personal.
One author commits as
48211903+dhaverford@users.noreply.github.com
. Resolving the numeric ID returns a different current username — the account was renamed, and the old handle links to conference talks and a personal blog the current one does not.
The dead end: gitleaks flags an AWS-shaped key in a test fixture, and searching the string finds it verbatim in AWS's own documentation. Discarded, and recorded as discarded so nobody re-raises it.
The real finding is a
terraform.tfstate
added and deleted in the same week two years ago, recovered from history. No live credential, but four internal hostnames and a cloud account ID — infrastructure for
find-hidden-subdomains
.
Grade: team roster probable, since commit authorship is self-asserted and uncorroborated. The rename link confirmed — the numeric ID is immutable.
目标:绘制一家仅在官网列出高管的公司的工程团队图谱。
对内部域名
svc.example-corp.net
进行代码搜索,返回三个仓库,均未包含公司名称。镜像克隆这些仓库后,得到14个不同的作者邮箱,其中11个是企业邮箱,3个是个人邮箱。
某作者的提交邮箱为
48211903+dhaverford@users.noreply.github.com
。解析该数字ID后得到一个不同的当前用户名——该账号已更名,旧用户名关联到当前用户名未公开的会议演讲和个人博客。
无效发现:gitleaks在测试fixture中标记了一个类似AWS密钥的字符串,搜索该字符串后发现它完全匹配AWS官方文档中的示例。该发现被排除,并记录排除原因以避免重复上报。
真实发现:两年前同一周内添加又删除的
terraform.tfstate
文件,从历史记录中恢复。虽然没有有效凭证,但包含四个内部主机名和一个云账号ID——对应
find-hidden-subdomains
的基础设施。
分级:团队名单为大概率,因为提交作者身份是自行声明且未得到佐证。更名关联为已确认——数字ID是不可变的。

Pivots

关联拓展

New selectorGoes to
Commit author emails
what-an-email-reveals
,
what-leaked-about-you
Handles, renamed accounts, numeric IDs
hunt-a-handle
Real names from commits and CODEOWNERS
find-anyone
Internal hostnames from config
find-hidden-subdomains
,
who-owns-this-domain
Cloud account IDs, buckets, IPs
find-exposed-servers
,
google-like-a-spy
Committed documents and images
secrets-in-file-metadata
Org structure and vendor stack
x-ray-a-company
Deleted repos and removed pages
read-deleted-pages
Credential appearing in a public dump
find-leaks-in-the-wild
The contributor network
graph-the-network
新筛选条件关联内容
提交作者邮箱
what-an-email-reveals
,
what-leaked-about-you
用户名、更名账号、数字ID
hunt-a-handle
提交记录和CODEOWNERS中的真实姓名
find-anyone
配置文件中的内部主机名
find-hidden-subdomains
,
who-owns-this-domain
云账号ID、存储桶、IP地址
find-exposed-servers
,
google-like-a-spy
提交的文档和图片
secrets-in-file-metadata
组织架构和供应商栈
x-ray-a-company
已删除的仓库和页面
read-deleted-pages
公开泄露中的凭证
find-leaks-in-the-wild
贡献者网络
graph-the-network

Legal, disclosure, and the one hard rule

法律、披露与硬性规则

Never authenticate with a credential you find. Not to check whether it works, not read-only, not once. It is unauthorized access under computer-misuse law in most jurisdictions, and publication does not confer permission. Establish validity from format, context and metadata and nothing else. See ../../ETHICS.md.
A live-looking credential creates an obligation to disclose. Report promptly through a security contact, a
security.txt
route, or the host's vulnerability channel; give the repository, commit SHA and file path; state explicitly that history rewriting is not sufficient and the credential must be rotated; keep only a redacted reference; publish nothing. Hosts running automated secret-scanning partnerships may have revoked the token already, which does not remove the obligation to tell someone.
Cloning public repositories is normal use; bulk cloning and high-rate API querying are rate-limited and can breach terms. Commit emails are personal data under GDPR and equivalent regimes whatever the repository's status — collect the minimum and set a retention period.
**绝不要使用你发现的凭证进行认证。**无论是验证是否有效、只读访问,还是仅一次尝试。这在大多数司法管辖区都属于未经授权的访问,违反计算机滥用法律,且公开并不意味着获得权限。仅通过格式、上下文和元数据确认有效性,不要使用其他方式。详情请见../../ETHICS.md
看似有效的凭证会产生披露义务。请立即通过安全联系人、
security.txt
渠道或托管平台的漏洞上报通道提交报告;提供仓库、提交SHA和文件路径;明确说明仅重写历史记录不足以解决问题,必须轮换凭证;仅保留编辑后的参考信息;不得公开任何内容。运行自动机密扫描合作项目的平台可能已撤销该令牌,但这并不免除你告知相关方的义务。
克隆公共仓库属于正常使用;批量克隆和高频率API查询会受到速率限制,且可能违反服务条款。无论仓库状态如何,提交邮箱在GDPR及类似法规下都属于个人数据——请仅收集必要信息,并设置保留期限。