.git 源码泄露排查与防御实战配置

适用场景

.git 源码泄露是中小站点最常见的高危问题之一:运维用 Git 管理代码后,直接把整个工作目录(连同隐藏的 .git 版本库)部署到 Web 根目录,任何访问者请求 /.git/HEAD 都能拿到内容。本文适用于需要自查与修复源码泄露的站长、运维工程师,覆盖信息收集阶段的资产自查、漏洞排查,以及修复后的加固与验证。

出现下列任一情况,应立即执行本文的自查流程:站点用 Git 做版本管理且部署目录未做清理;使用宝塔面板一键拉取代码或整站打包;CDN 之外的源站可以直接访问(未做回源白名单);备份压缩包、.env、.svn 等文件同样位于 Web 根目录。

前置条件

  • 具备目标站点的 SSH 登录权限,能查看并修改 Web 目录与 Nginx/Apache 配置
  • 本机具备 curl 与 Python 3(用于批量探测脚本)
  • Web 服务为 Nginx 或 Apache;IIS 场景给出等价配置
  • 修复前先对站点目录做一次快照或备份,避免误删业务文件

原理说明

.git 目录结构固定:HEAD 指向当前分支,config 保存仓库配置与远端地址,objects/ 以 zlib 压缩存放全部对象(commit、tree、blob),refs/ 与 logs/ 保存引用与引用变更日志。只要 Web 服务器把 .git 当作普通静态目录对外提供,攻击者就能按固定路径逐个拉取这些文件,在本地还原出完整源码仓库,其中还包括已经删除但仍留在提交历史中的文件与硬编码凭证。

三个细节放大了危害等级:其一,logs/HEAD 保留了每次提交的作者名与邮箱,可为进一步的钓鱼与撞库提供素材;其二,config 中的 remote 地址可能携带账号令牌,直接给到代码托管平台的访问入口;其三,检出历史里的 .env、数据库连接串、对象存储密钥不会因为工作目录删除而消失。

触发路径通常有三类。第一类是部署方式问题:git clone 到站点目录后直接作为 Web 根目录,.git 一并对外。第二类是服务器配置问题:静态资源目录使用了过宽的 location / 规则,或者只拦截了 .htaccess 却漏掉 .git。第三类是备份残留:打包上传、复制粘贴、面板迁移过程中把 .git、.svn、.env、*.zip 一起带到了可访问目录。修复的核心不是删文件,而是让 Web 层默认拒绝所有以点开头的隐藏路径与常见备份后缀。

操作步骤

1. 远端探测暴露面

从公网侧确认是否可直接下载版本库文件,重点看 HTTP 状态码:200 表示文件真实存在,403 表示被规则拦截但文件仍在磁盘上,404 才是安全状态。

curl -s -o /dev/null -w "%{http_code} /.git/HEAD\n" https://example.com/.git/HEAD
curl -s -o /dev/null -w "%{http_code} /.git/config\n" https://example.com/.git/config
curl -s -o /dev/null -w "%{http_code} /.env\n" https://example.com/.env
curl -s -o /dev/null -w "%{http_code} /www.zip\n" https://example.com/www.zip
curl -s https://example.com/.git/config

2. 批量自查脚本

把常见敏感路径做成字典批量探测,一次拿到完整清单,避免遗漏。

import urllib.request, ssl

PATHS = [".git/HEAD", ".git/config", ".git/logs/HEAD", ".svn/entries", ".env",
         ".env.bak", "web.config", "config.php.bak", "www.zip", "backup.zip",
         "database.sql", ".DS_Store", ".idea/workspace.xml"]

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

def probe(base, path):
    url = base.rstrip("/") + "/" + path
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
        with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
            return r.status, len(r.read())
    except Exception as e:
        return getattr(e, "code", "ERR"), 0

for p in PATHS:
    code, size = probe("https://example.com", p)
    flag = "!! EXPOSED" if code == 200 else ""
    print(f"{code:>4} {size:>8} /{p} {flag}")

状态码为 200 且响应长度大于 0 的条目即为真实可下载文件,需要立即处理;403 说明文件仍在服务器上,虽然暂时读不到,但换路径或换编码仍可能绕过,同样应按泄露处理。

3. 评估已泄露范围

若 /.git/ 可读,攻击者会用开源工具把整个仓库拉回本地,命令形态如下(仅用于自证风险,请勿对未授权目标执行):

# 攻击者视角:还原暴露的版本库
pip install git-dumper
git-dumper https://example.com/.git/ ./leaked-src
cd leaked-src && git log --oneline | head
git log --all --diff-filter=D --name-only | head -50

还原后可直接检出历史版本,把已删除的配置文件重新取出。因此一旦确认可读,就必须按”凭证已泄露”处置:轮换数据库密码、对象存储 AK/SK、第三方 API Key、SSH 私钥与邮件账号口令,不能只删目录了事。

4. Nginx 层拦截规则

在站点 server 块内加入统一的隐藏文件与备份后缀拒绝规则,优先级高于静态资源匹配。

server {
    listen 443 ssl;
    server_name example.com;
    root /www/wwwroot/example.com;

    # 拒绝所有点开头的隐藏文件与目录(保留 ACME 校验目录)
    location ~ /\.(?!well-known) {
        deny all;
        access_log off;
        log_not_found off;
        return 404;
    }

    # 拒绝常见备份、源码包与数据库导出文件
    location ~* \.(zip|tar|gz|tgz|bak|old|sql|swp|swo)$ {
        deny all;
        return 404;
    }

    location ~* /(composer\.(json|lock)|package\.json|yarn\.lock)$ {
        deny all;
        return 404;
    }
}

注意三点:return 404 优于 deny all,避免暴露”此处有敏感文件”的信号;(?!well-known) 必须保留,否则会阻断 Let’s Encrypt 的证书签发校验;规则写在 include 的通用配置里只对新站点生效,存量站点需要逐个确认已加载。

5. Apache 与 IIS 等价配置

Apache 在站点 DocumentRoot 的 .htaccess 中加入规则,或写进虚拟主机配置:

RedirectMatch 404 /\.git
RedirectMatch 404 /\.svn
RedirectMatch 404 /\.env
<FilesMatch "\.(bak|old|sql|zip|tar|gz)$">
    Require all denied
</FilesMatch>

IIS 在站点 web.config 的 system.webServer 节点中加入请求过滤,禁止隐藏段与备份后缀:

<security>
  <requestFiltering>
    <hiddenSegments>
      <add segment=".git" />
      <add segment=".svn" />
    </hiddenSegments>
    <fileExtensions>
      <add fileExtension=".sql" allowed="false" />
      <add fileExtension=".bak" allowed="false" />
    </fileExtensions>
  </requestFiltering>
</security>

6. 部署流程根治

Web 层拦截只是兜底,源头必须让 .git 根本不进入可访问目录,推荐两类做法:

  • 分离部署:Git 仓库放在 /opt/src/example,用 git archive 或 CI 产物发布到 Web 目录,发布目录内不含版本库。
  • 排除同步:使用 rsync 时显式排除,避免整目录拷贝把隐藏文件带过去。
# 方式一:只导出受版本控制的文件,天然不含 .git
cd /opt/src/example
git archive --format=tar HEAD | tar -x -C /www/wwwroot/example.com

# 方式二:rsync 发布,显式排除隐藏文件与备份
rsync -av --delete \
  --exclude='.git' --exclude='.svn' --exclude='.env' \
  --exclude='*.log' --exclude='*.sql' --exclude='*.zip' \
  /opt/src/example/ /www/wwwroot/example.com/

# 发布后校验:确认发布目录中没有版本库
find /www/wwwroot/example.com -maxdepth 3 -name '.git' -o -name '.svn' -o -name '.env'

若使用宝塔面板,站点目录压缩下载、目录复制、一键迁移都可能带上隐藏文件,操作后需重新执行上一条 find 命令复核。

7. 已泄露后的清理

若敏感信息确实存在于 Git 历史中,普通删除提交无效,需要用 filter-repo 重写历史并强制推送,再通知所有协作者重新克隆:

pip install git-filter-repo
cd /opt/src/example
git filter-repo --path .env --invert-paths
git push --force --all
git push --force --tags

配置验证

  • 语法与加载:nginx -t 通过后 nginx -s reload;Apache 用 apachectl configtest。
  • 拦截生效:重复第 1 步的 curl 探测,/.git/HEAD 与 /.env 必须返回 404。
  • 业务不受影响:首页、列表页、静态资源、证书自动续期分别访问一次,确认 200。
  • 批量复测:重复第 2 步脚本,确认 EXPOSED 条目为 0。
  • 磁盘复核:确认服务器上确实不存在该目录,避免”只做了拦截、文件仍在”。

常见问题

Q1:只删掉了 .git 目录,为什么还说有风险?

删除只解决”当前可下载”的问题,无法收回已经泄露的内容。如果暴露窗口内被爬虫或扫描器命中,源码与其中的凭证其实已经流出。正确顺序是先轮换全部凭证(数据库、对象存储、第三方 API、SSH 密钥),再删除目录,最后补上 Web 层拦截与部署流程改造。

Q2:加了 location 规则后首页出现 403,如何排查?

常见原因是正则范围写得太宽。例如写成 location ~ \. 会命中带点的正常 URL(如 /news/2026.html),location ~ /\.(?!well-known) 或限定后缀的写法才是安全的。排查时用 nginx -T 导出最终生效配置,确认规则与 location 匹配顺序,再对具体 URL 用 curl 单点验证。

Q3:CDN 下探测不到泄露,是不是就安全了?

不是。CDN 只改变访问入口,源站仍然可达,攻击者可直接通过源站 IP 或用真实 Host 头回源验证。源站必须限制仅允许 CDN 回源 IP 访问,同时把上述拦截规则部署在源站 Nginx 上,两者缺一不可。

总结

源码泄露的处置遵循”探测—评估—拦截—根治—验证”五步:先用状态码判断暴露面,再用批量脚本拿到完整清单;确认可读后按凭证已泄露处置并轮换密钥;Nginx/Apache/IIS 三端统一拒绝隐藏路径与备份后缀,返回 404 而非 403;部署环节改用 git archive 或 rsync 排除规则,从源头让 .git 不进入 Web 目录。规则上线后务必复测状态码与业务可用性,并把探测脚本纳入月度例行自查。