secrets-in-git-history
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSecrets 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 have | Start with | Why |
|---|---|---|
| An organisation name | Public org members, then their personal repos | Corporate repos are reviewed; personal ones are not |
| A developer's name or handle | Commit emails across their repos | Yields an email nothing else gives you |
| A commit email | Reverse-search across hosts; resolve any noreply ID | Links accounts across orgs and platforms |
| A suspected credential leak | Both scanners over a mirror clone, all refs | HEAD-only scanning misses the point entirely |
| A specific string — hostname, key prefix | | Tells you when it entered and when it left |
| A live domain | Check for an exposed | Full history from a web server, if authorized |
| Nothing but a company website | Code search the org's internal domain names | Finds repos with no obvious link to the org |
| 你拥有的信息 | 入手点 | 原因 |
|---|---|---|
| 组织名称 | 公开组织成员,再到他们的个人仓库 | 企业仓库会经过审核;个人仓库则不会 |
| 开发者姓名或用户名 | 跨仓库提取提交邮箱 | 能获取其他渠道无法提供的邮箱 |
| 提交邮箱 | 跨平台反向搜索;解析noreply ID | 关联不同组织和平台的账号 |
| 疑似凭证泄露 | 在镜像克隆仓库上运行两款扫描工具,覆盖所有引用 | 仅扫描HEAD完全偏离了检测核心 |
| 特定字符串——主机名、密钥前缀 | 跨所有引用执行 | 能告诉你该字符串何时加入、何时移除 |
| 活跃域名 | 检查是否存在暴露的 | 若获得授权,可从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 -uGitHub's web UI does not display commit emails, which leads people to assume
they are hidden. They are not. Append to any commit URL for a
mail-format patch whose header carries the author name and email; the
API returns the same under .
.patchFrom:commit.author.emailbash
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.authorThe noreply pattern is the part worth understanding properly. Users with
email privacy enabled commit as , or in the
newer form . 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:
username@users.noreply.github.com12345678+username@users.noreply.github.combash
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 -uGitHub的Web界面不会显示提交邮箱,这让人们误以为邮箱是隐藏的。实际上并非如此。在任意提交URL后追加,即可获取邮件格式的补丁文件,其头包含作者姓名和邮箱;API也会在字段下返回相同信息。
.patchFrom:commit.author.emailbash
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.authornoreply模式值得深入理解。开启邮箱隐私保护的用户提交代码时,邮箱格式为,或更新的格式。第二种格式会泄露账号的数字用户ID,该ID是不可变的,无论账号多少次更名都不会改变,因此旧提交可以关联到当前持有该ID的用户:
username@users.noreply.github.com12345678+username@users.noreply.github.combash
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:
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.
https://api.github.com/orgs/<org>/public_membersFrom 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 with it,
and from , and the object store the entire history is
reconstructible — source, credentials, author identities, everything.
.git.git/config.git/HEADObserving that such a path exists, through a search index or an existing scan
result, is passive. Retrieving it is not. Downloading a directory is
accessing a system and taking data from it, and no misconfiguration constitutes
permission. Only inside a written authorization; otherwise, report it.
.git将工作目录复制到Web根目录的部署方式会同时发布目录,通过、和对象存储可重建完整历史记录——包括源代码、凭证、作者身份等所有信息。
.git.git/config.git/HEAD通过搜索索引或现有扫描结果发现此类路径的存在属于被动行为。**但检索该目录则不属于被动行为。**下载目录属于访问系统并获取数据,任何配置错误都不构成访问权限。仅在获得书面授权的情况下才可操作;否则,请上报该问题。
.gitWhere 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 returns three
repositories, none carrying the company's name. Mirror-cloning them gives
fourteen distinct author emails, eleven corporate and three personal.
svc.example-corp.netOne author commits as . 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.
48211903+dhaverford@users.noreply.github.comThe 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 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 .
terraform.tfstatefind-hidden-subdomainsGrade: team roster probable, since commit authorship is self-asserted and
uncorroborated. The rename link confirmed — the numeric ID is immutable.
目标:绘制一家仅在官网列出高管的公司的工程团队图谱。
对内部域名进行代码搜索,返回三个仓库,均未包含公司名称。镜像克隆这些仓库后,得到14个不同的作者邮箱,其中11个是企业邮箱,3个是个人邮箱。
svc.example-corp.net某作者的提交邮箱为。解析该数字ID后得到一个不同的当前用户名——该账号已更名,旧用户名关联到当前用户名未公开的会议演讲和个人博客。
48211903+dhaverford@users.noreply.github.com无效发现:gitleaks在测试fixture中标记了一个类似AWS密钥的字符串,搜索该字符串后发现它完全匹配AWS官方文档中的示例。该发现被排除,并记录排除原因以避免重复上报。
真实发现:两年前同一周内添加又删除的文件,从历史记录中恢复。虽然没有有效凭证,但包含四个内部主机名和一个云账号ID——对应的基础设施。
terraform.tfstatefind-hidden-subdomains分级:团队名单为大概率,因为提交作者身份是自行声明且未得到佐证。更名关联为已确认——数字ID是不可变的。
Pivots
关联拓展
| New selector | Goes to |
|---|---|
| Commit author emails | |
| Handles, renamed accounts, numeric IDs | |
| Real names from commits and CODEOWNERS | |
| Internal hostnames from config | |
| Cloud account IDs, buckets, IPs | |
| Committed documents and images | |
| Org structure and vendor stack | |
| Deleted repos and removed pages | |
| Credential appearing in a public dump | |
| The contributor network | |
| 新筛选条件 | 关联内容 |
|---|---|
| 提交作者邮箱 | |
| 用户名、更名账号、数字ID | |
| 提交记录和CODEOWNERS中的真实姓名 | |
| 配置文件中的内部主机名 | |
| 云账号ID、存储桶、IP地址 | |
| 提交的文档和图片 | |
| 组织架构和供应商栈 | |
| 已删除的仓库和页面 | |
| 公开泄露中的凭证 | |
| 贡献者网络 | |
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 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.
security.txtCloning 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。
看似有效的凭证会产生披露义务。请立即通过安全联系人、渠道或托管平台的漏洞上报通道提交报告;提供仓库、提交SHA和文件路径;明确说明仅重写历史记录不足以解决问题,必须轮换凭证;仅保留编辑后的参考信息;不得公开任何内容。运行自动机密扫描合作项目的平台可能已撤销该令牌,但这并不免除你告知相关方的义务。
security.txt克隆公共仓库属于正常使用;批量克隆和高频率API查询会受到速率限制,且可能违反服务条款。无论仓库状态如何,提交邮箱在GDPR及类似法规下都属于个人数据——请仅收集必要信息,并设置保留期限。