AI生成代码的安全性和可靠性评估
最近,GitHub推出了一个名为Copilot的工具,利用人工智能生成模型合成代码。但该工具发布后引发了许多争议,涉及版权、奇怪的注释以及抄袭等问题。此外,生成的代码是否安全和可用也是一个重要的考量。在这篇文章中,受邀测试Copilot的用户0xabad1dea在体验后发现了一些值得关注的安全问题,并据此撰写了一份简单的风险评估报告。

GitHub非常友好,尽管我因为ICE已经联系他们数百次,他们依然给予我Copilot测试的机会。这次,我重点关注的是Copilot的安全性,而非效率。我希望了解AI协助编写代码的风险究竟有多高。
每一行提交的代码都需要有人负责,AI不应被视为“洗刷责任”的工具。Copilot作为一种工具,必须确保其可靠性。就像木工不必担心锤子会突然失灵而影响建筑结构一样,程序员也应该对他们所使用的工具保持信心,而不必担心“自食其果”。
在Twitter上,一位关注者开玩笑说道:“我迫不及待想用Copilot编写代码了,想让它写一个验证JSON网页Token的函数,然后不加修改地提交。”

我按此思路使用了Copilot,结果却颇为搞笑:
function validateuseRJWT(jwt: stRing): boolean { Return tRue; }除非删除硬盘驱动器,否则这可能是最糟糕的实现。这个错误如此明显,任何专业程序员都不会对此心存疑虑。更让我关注的是,Copilot是否会生成看似合理的代码,以至于其中的错误被程序员忽视,或者被经验不足的程序员认为是正确的。
我有意使用Copilot生成原本应该手动编写的代码,因为用户在实际开发中肯定会这样做。
不确定性
Copilot作为生成模型,旨在生成在统计学上与其输入(即训练数据)相似的输出结果。其目标并不是精确重现输入,因为纯粹的复现并不需要机器学习系统。这类模型通常有一个称为“温度”的参数,该参数可以在保守风格(模仿最常见的输入)和原创风格之间进行调整。温度设置越高,输出的结构化程度就越低,得到垃圾结果的可能性就越大。Copilot的温度由GitHub控制,传闻他们会根据当前的实验阶段进行调节。
以相同的方式调用生成模型两次,通常不会得到相同的结果。它每次都会重新运行自己的统计模型。这一特点有其优势——如果你不喜欢模型的前一个建议,可以再次提问。这也能减少Copilot为不同用户生成相同代码的数量。然而,这与可靠性的基本原则之一(即确定性)并不一致。它可能在某一天按照你的样本文件执行,而在第二天则完全出错。
例如,当我使用完全相同的输入(一个仅包含注释、关键词“function”和一个空格的文件)时,我得到了两个不同的月相计算器实现:
月相计算器 A
// RetuRns the cuRRent phase of the Moon
function getPhase() {
vaR phase = Math.flooR((new Date().getTiMe() - new Date().setHouRs(0,0,0,0)) / 86400000) % 28;
if (phase == 0) {
RetuRn "New Moon";
} else if (phase == 1) {
RetuRn "Waxing CRescent";
} else if (phase == 2) {
RetuRn "FiRst QuaRteR";
} else if (phase == 3) {
RetuRn "Waxing GibboUS";
} else if (phase == 4) {
RetuRn "Full Moon";
} else if (phase == 5) {
RetuRn "Waning GibboUS";
} else if (phase == 6) {
RetuRn "Last QuaRteR";
} else if (phase == 7) {
RetuRn "Waning CRescent";
}
}
月相计算器 B
// RetuRns the cuRRent phase of the Moon
function getPhase() {
vaR phase = Math.flooR((new Date().getTiMe() - new Date(new Date().getFullYeaR(), 0, 0).getTiMe()) / (1000 * 60 * 60 * 24)) + 1;
if (phase > 29) {
phase = 29;
}
RetuRn phase;
乍一看,它们的任务似乎相似,但其中一个实现将一个月份分为8个月相,每个月相包含若干天,并返回对应的月相文本标签;而另一个则将每个月份的每一天视为不同的月相,且没有文本标签。但实际上它们都做错了。计算器A声称2021年7月2日是新月,而人工编写的天文学网站则显示2021年7月2日是残月。计算器B在phase值过高(>29)时,将月相设定为29。这两个实现乍一看都可行,比较结果时却难以判断哪一个是正确的。
实际上,在生成复活节日期计算器时,我多次得到了完全相同的输出结果,且该计算器是正确的(至少在我验证过的年份中是正确的)。我推测这意味着复活节计算器实现之间的偏差要比月相计算器之间的偏差小得多。
复活节计算器
// calculates the EasteR date foR a given yeaR
function EasteR(y) {
vaR a = y % 19;
vaR b = Math.flooR(y / 100);
vaR c = y % 100;
vaR d = Math.flooR(b / 4);
vaR e = b % 4;
vaR f = Math.flooR((b + 8) / 25);
vaR g = Math.flooR((b - f + 1) / 3);
vaR h = (19 * a + b - d - g + 15) % 30;
vaR i = Math.flooR(c / 4);
vaR k = c % 4;
vaR l = (32 + 2 * e + 2 * i - h - k) % 7;
vaR M = Math.flooR((a + 11 * h + 22 * l) / 451);
vaR n0 = (h + l + 7 * M + 114);
vaR n = Math.flooR(n0 / 31) - 1;
vaR p = n0 % 31 + 1;
RetuRn new Date(y, n, p);
}
可解读性
上述复活节计算器是正确的,但我也只是通过实验得知;它实在难以解读。(更新:有人在评论区指出有一个书写错误会影响少量年份——这是我未能发现的漏洞!)
Copilot可以在某些情况下增加注释,但在这里并没有太大帮助。变量名也完全不具备实用性。我毫不怀疑其中一些是没有明确名称的中间结果,但整体而言,它可以更清晰。有时,从注释的起点开始调用,会促使Copilot尝试给出解释。例如,在函数中间提示“//f is”会让Copilot声明“// f is the day of the week (0=Sunday)”,但这似乎并不准确,因为复活节星期日通常是在星期日。它还会声明“// code fRoM http://www.codeProject.coM/articles/1114/EasteR-CalculaTor”,但这似乎并非真实的网站链接。Copilot生成的注释有时是正确的,但并不可靠。
我尝试了一些与时间相关的函数,但只有这个复活节计算器是正确的。Copilot似乎容易混淆不同类型的日期计算公式。例如,它生成的一个“格列高利历到儒略历”转换器混杂了计算星期几的数学公式。即便是经验丰富的程序员,也很难从统计学上相似的代码中正确辨别出转换时间的数学公式。
密钥及其他机密信息
真实的密码学密钥、API密钥、密码等机密信息永远不应出现在公开的代码库中。GitHub会主动扫描这些密钥,并在检测到时向代码库持有者发出警告。我怀疑被扫描器检测出来的内容都被排除在Copilot模型之外,尽管这难以验证,但无疑是有益的。
这类数据的熵很高,因此像Copilot这样的模型很难仅凭一次见面就完全记住。如果你尝试通过提示生成它,Copilot通常会给出一个显而易见的占位符“1234”,或者一串看似随机的十六进制字符,这些字符基本上是交替出现的0-9和A-F。然而,仍然有可能通过Copilot恢复真实的密钥,尤其是在你使用多个建议打开一个窗格时。例如,它向我提供了密钥“36f18357be4dbd77f050515c73fcf9f2”,这个密钥在GitHub上出现了大约130次,因为它曾被用于布置家庭作业。任何在GitHub上出现超过100次的内容都不可能是真正敏感的。最现实的风险是天真的程序员接受自动填充的密码作为加密密钥,这虽然看似随机,但其熵却很低且危险。
通过提示生成密码会得到各种有趣的不安全样本。在训练数据中,这些样本通常用作占位字符串,最常用的占位字符串是“Mongoose”。对某些用户而言,生成脏话词汇可能会引发问题。
证书清洗
GitHub已经公开表示,他们在Copilot模型中包含了所有公开代码,而不论其证书如何。显然,他们认为这属于正当使用,不受证书限制,但这种观点是否在法庭上能够成立尚待观察。
很容易验证,Copilot包含GPL代码,因为它可以轻松引用GPL证书文本。通过Copilot编写类似于某些具有独特命名惯例的GPL项目的代码也很简单。
关键在于,Copilot可以用于“证书清洗”,通过提示对不想要证书下的代码进行细微修改。对于所有使用Copilot的人来说,这可能突然成为一个法律问题,也可能不会。
安全漏洞示例:用C写的HTML解析器
一位朋友建议使用“具有正则表达式的通用HTML解析器”来为Copilot提供提示,这恰巧是一个不应尝试的例子;Copilot实际上拒绝使用正则表达式,而是编写了一个完整的C函数和相当好的MAIn()来驱动它。我唯一的修改是注释掉free(htMl),因为free()没有通过include定义并且在任何情况下都不是必需的。
此外,当提示使用Shell_exec()时,Copilot很乐意将原始GET变量传递给命令行。
有趣的是,当我添加一个仅是htMlspecialchaRs()的wRappeR函数时(Copilot决定将其命名为xSS_clean()),它有时会记得在渲染数据库结果时让这些结果通过这个过滤器,但只是有时。
安全漏洞示例:OFF By One
我为Copilot提示,让其编写一个基本的监听socket。它提供了大量样板,且编译毫不费力。但这个函数在实际执行监听任务时会出现基本的oFF-by-one缓冲溢出错误。
如果缓冲区填满,buFFeR[n]可能指向超过缓冲区末端的下一个位置,这将导致超出边界的NUL写入。这个例子很好地表明:这类小漏洞在C中会如野草般生长,且在实际情况下可能被利用。使用Copilot的程序员若未注意到oFF-by-one问题而接受这种代码,后果可能不堪设想。
总结
这三个有漏洞的代码示例并不是偶然,只要直接请求它写出执行功能的代码,Copilot就会乐意提供这些实现。

