起源
Git 炸弹因 XML 炸弹(AKA "billion laughs")而得名,所以要理解 Git 炸弹,我们不妨先从 XML 炸弹说起。
下面是一段 XML 文件示例:
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol"laugh out loud">
<!ENTITY lol2"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8"&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9"&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
从 ENTITY 字段看起,每行 ENTITY 代表一个 XML 实体(entity)元素,一共10个实体元素,除了第一个实体 lol 定义了字符串"laugh out loud"(AKA lol)外, 其余实体都是实体引用,它们每个都引用自上一个实体,且重复10次。
此 XML 文件的文档内容部分仅包含对实体lol9的一个实例的引用。但是,当它被 DOM 或 SAX 解析器解析时,遇到lol9时,它会扩展为 10 个lol8,而每个lol8会扩展为10 个lol7,依此类推。到将所有内容扩展为文本lol时,字符串"laugh out loud"的数量已达100,000,000。如果再增加一个类似结构的实体,或者第一个实体lol被定义为10个"laugh out loud"字符串的话,那么将有10亿个"laugh out loud",即十亿大笑。
这段文本看似内容不多,占用内存不大,但是在解析过程中内容被指数级展开,会消耗大量内存资源,所以有人利用这个原理进行DOS攻击,使被攻击的机器的内存迅速耗尽,从而停止服务。
以上这种 XML 攻击,被称作 XML 炸弹。
Git 炸弹的原理大致也跟XML炸弹类似,它利用了 Git 的某种特性,使得重复的文本内容深度嵌套。所以接下来再来看一下 Git 炸弹的原理。
Git 炸弹原理
我们都知道 Git 的基本数据结构有commit,tag, tree, blob, blob 只存储文件内容,tree 存储文件名称,文件目录结构, commit 与 tag 类似于一种引用(reference),指向 tree。
每中数据都有自己的hash ID, 所以对于blob来说,只要其中的内容是一样的,那么其 ID 就是一样的,不管其内容的文件名,文件路径是否相同。换句话说,Git消除了blob的重复,允许不同的文件(文件名,文件路径不同)使用相同的blob,目的是减少文件内容的重复。对于tree也是类似。
所以有人就利用这个特性制作了一个 Git 仓库, 其结构类似:
之后,只要运行包含树的遍历操作的 Git 命令,如git status, git checkout 等命令,Git 会先在内存中构造出该仓库的树结构,在这种特殊的仓库中,这个过程会消耗大量内存,因此只要这个仓库的树嵌套足够深,内存就会马上被消耗完,相关进程会被终止。
与XML炸弹类似,只要这种嵌套结构达到10层,或者底层的 blob 有10个,则整个过程展开会有 10 亿条 tree(路径)。
制作
Git炸弹第一次公开讨论是在 2017 年,Kate 在自己的博客[2]中讨论了制作 Git 炸弹的原理以及制作方法,制作程序是用 Python 写的,见:
如何防止
经过 Github 以及漏洞平台 Hackerone 同意后,Kate 公开了自己在 Github 上自己的 git-bomb仓库[3],并且在自己的博客中公开讨论了 Git 炸弹的相关信息,之后马上引起了 Git 上游社区的关注,并且马上讨论了可能的修复方案: [4]
- Git上游社区,最终补丁: [5]
Git 官方的修复是将遍历树的过程变得更快,使得对 Git 炸弹仓库做任何操作不至于等待很久。
其commit message写到:
You can see this in a pathological case where a commit adds
a very large number of entries, and we limit based on a
broad pathspec. E.g.:
perl -e '
chomp(my $blob = `git hash-object -w --stdin </dev/null`);
for my $a (1..1000) {
for my $b (1..1000) {
print "100644 $blob\t$a/$b\n";
}
}
' | git update-index --index-info
git commit -qm add
git rev-list HEAD -- .
This case takes about 100ms now, but after this patch only
needs 6ms. That's not a huge improvement, but it's easy to
get and it protects us against even more pathological cases
(e.g., going from 1 million to 10 million files would take
ten times as long with the current code, but not increase at
all after this patch).
这个修复并没有解决处理这种仓库导致内存消耗过大问题,只对处理过程的消耗时间做了一次优化。