最近,它推出了一款利用人工智能生成模型来合成代码的工具。 然而,自发布以来就饱受争议,包括版权纠纷、怪异评论和涉嫌抄袭等。 另外,生成的代码能不能用、敢不敢用也是一个大问题。 本文中,受邀测试的用户在试用代码合成工具后发现了一些值得关注的安全问题,并据此写了一份简单的风险评估报告。
很高兴的是,尽管我已经就 ICE 问题向他们进行了数百次窃听,但他们仍然允许我进入测试阶段。 这次我不关心效率,只是想测试一下它的安全性。 我想知道让人工智能替人写代码有多大风险。
人类需要对提交的每一行代码负责,不应该用人工智能来“洗刷责任”。 它是一个工具,工具必须可靠才可以使用。 木匠不必担心他们的锤子突然坏掉并导致建筑物的结构缺陷。 同样,程序开发人员应该对他们的工具保持信心,而不必担心“搬起石头砸自己的脚”。
在 上,我的一位关注者开玩笑说:“我迫不及待地想在其中编写代码。我希望它编写一个验证 JSON 网页令牌的函数,然后看也不看就提交。”
我按照这个思路来使用,结果很搞笑:
function validateUserJWT(jwt: string): boolean {
return true;
}
除了删除硬盘之外,这可能是最糟糕的事情。 这个错误是如此明显和粗暴,任何专业程序员都不会对此有任何问题。 我更感兴趣的是,生成的代码是否乍一看看起来很合理,这样其中的错误就会被程序员忽视,或者被经验不足的程序员认为是正确的。 (剧透警告:这种情况确实发生了。)
我有意使用生成实际上应该由人类编写的代码,因为用户肯定会在生产中这样做。
不确定
是一个生成模型,这意味着它的目的是产生统计上近似其输入(即训练数据)的输出。 目标不是准确地再现其输入,因为纯粹的再现不需要机器学习系统。 此类模型具有一个通常称为“temp()”的参数,它是一个可以在保守风格(模仿最常见的输入)和原始风格之间调整的滑动比例。 原创性设置越高,输出的结构化程度就越低,并且获得垃圾结果的可能性就越大。 温度是有控制的,据说他们会根据现阶段的试验进行调整。
以相同的方式调用生成模型两次通常不会产生相同的结果。 它每次都会重新运行其统计模型。 这样做有其优点——如果您不喜欢模特之前的建议,您可以再次询问。 这也减少了为不同用户生成的相同代码的数量。 然而,这不符合可靠性最基本的原则之一:确定性。 它有可能有一天完全按照您的示例文件执行的操作,但第二天就完全错误了。
例如,使用完全相同的输入(仅包含注释、关键字“”和空格的文件),我得到了月相计算器的以下两种不同实现:
月相计算器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日是亏月。 当相位值高得离谱 (>29) 时,计算器 B 将月相设置为 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);
}
可解释性
上面的复活节计算器是正确的,但我只是通过实验才知道; 太难破译了。 (更新:有人在评论部分指出,有一个影响了少数年份的笔误——一个逃过了我检查的错误!)
有时可以添加注释,但它们在这里没有影响。 里面的变量名也完全没用。 我毫不怀疑其中一些是中间结果,没有明确的名称,但总的来说,它使得更加清晰。 有时,回调到注释的开头会尝试给出解释。 例如,提示 //f is in a 会生成语句 // f is the day of the week (0=),但这似乎不正确,因为复活节星期日 ( ) 往往是星期日。 它还指出 // Code from ,但这似乎不是指向真实网站的链接。 生成的注释有时是正确的,但不可靠。
我尝试了一些与时间相关的功能,但只有这个复活节计算器是正确的。 计算日期的不同类型的数学公式似乎很容易混淆。 例如,它生成的公历到儒略历转换器与用于计算星期几的数学公式混在一起。 即使对于经验丰富的程序员来说,也很难从统计上相似的代码中正确辨别转换时间的数学公式。
密钥和其他机密信息
真实加密密钥、API 密钥、密码等机密信息绝不应在公共代码库中发布。 这些密钥会被主动扫描,如果检测到它们,则会向存储库持有者发出警告。 我怀疑该扫描仪检测到的任何内容都被排除在模型之外,这很难验证,但肯定是有益的。
这类数据的熵(希望)很高,因此这样的模型很难在看到一次后完全记住它。 如果你尝试从提示符中生成它,它通常会给你一个明显的占位符“1234”或一串十六进制字符——乍一看看起来是随机的,但基本上是0-9和AF交替出现。 (不要故意使用它来生成随机数。它们的语法是结构化的,并且也可以向其他人建议相同的数字。)但是,仍然可以恢复真正的密钥,特别是如果您使用 10 而不是打开窗格时不建议。 例如,它为我提供了一个出现了大约 130 次的密钥,因为它用于家庭作业。 任何在网上出现超过 100 次的内容都不可能成为真正敏感的内容。 最真实的风险是天真的程序员收到自动填充的密码作为加密密钥,这将使结果值看起来是随机的,但熵却低得危险。
通过提示生成密码会产生各种有趣的不安全样本。 在训练数据中,这些样本通常用作占位符字符串。 每个人最喜欢的占位符字符串是“”。 生成脏话可能会给某些用户带来问题。
证书清洁
已公开声明,他们将网站上托管的所有公共代码包含在其模型中,无论证书如何。 很明显,他们认为这是合理使用,不受证书限制,但这种观点是否会在法庭上站得住脚......还有待观察。
由于可以轻松地从内存中引用 GPL 证书文本,因此可以轻松验证是否包含 GPL 代码。 编写看起来像某些具有独特命名约定的 GPL 项目的代码也很容易。
关键是它可以通过提示它对不需要的证书下的代码进行细微的修改来用于“证书清理”。 对于每个使用它的人来说,这可能会也可能不会突然成为一个大的法律问题。
安全漏洞示例:用 C 编写的 HTML 解析器
一位朋友建议使用“带有正则表达式的通用 HTML 解析器”来提供提示,这恰好是您不应该做的事情的一个示例; 实际上拒绝使用正则表达式,而是编写一个完整的 C 函数和相当好的 main() 来驱动它。 我所做的唯一修改是注释掉 free(html),因为 free() 不是根据定义定义的,并且在任何情况下都不需要。
#include
// general purpose HTML parser with regex
void parse(char *html) {
int i = 0;
while (html[i] != '\0') {
if (html[i] == '<') {
// find the end of the tag
int j = i + 1;
while (html[j] != '>') {
j++;
}
// find the end of the attribute
int k = j + 1;
while (html[k] != ' ' && html[k] != '\0') {
k++;
}
// copy the tag and attribute to a new string
char tag[j - i + 1];
char attr[k - j + 1];
for (int m = 0; m < j - i; m++) {
tag[m] = html[i + m];
}
tag[j - i] = '\0';
for (int m = 0; m < k - j; m++) {
attr[m] = html[j + m];
}
attr[k - j] = '\0';
printf("%s %s\n", tag, attr);
// skip the end of the tag
i = k;
} else {
i++;
}
}
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("usage: %s \n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
printf("error opening %s\n", argv[1]);
return 1;
}
char *html = NULL;
size_t len = 0;
if (getline(&html, &len, fp) == -1) {
printf("error reading %s\n", argv[1]);
return 1;
}
parse(html);
//free(html); // copilot inserted this but it's not declared and not necessary anyway
return 0;
}
这是格式良好、甚至带有注释的 C 代码,它看起来确实可以解析 HTML,并且它的主要函数有一些用于打开文件的有用样板。 但其解析过程存在很多问题。
好消息是,有大量精心设计的指针数学,其工作原理就像……以前已经设计过一样。 很难说这是预先设计的,因为这个函数实际上并没有做任何有用的事情,尽管它与基本解析器的功能有大约 80% 的相似度。 当然,运行到缓冲区末尾直接导致的崩溃是一个致命的安全问题。
安全漏洞示例:用 PHP 编写的 SQL 注入
前两行是我的提示。
<?php
// connects to the database
include('../db_connect.php');
// grabs the variables from the URL
$id = $_GET['id'];
// makes the connection to the database
$mysqli = new mysqli($hostname, $username, $password, $database);
// checks for errors
if(mysqli_connect_errno())
{
// we could not connect to the database, so output a message
echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
// executes the query
$query = "SELECT * FROM users WHERE id = '$id'";
$result = $mysqli->query($query);
...
这个样板文件出现了严重错误,导致了 2000 年代初期典型的安全漏洞之一:PHP 脚本获取原始 GET 变量并将其插入用作 SQL 查询的字符串中,从而导致 SQL 注入。 对于 PHP 初学者来说,犯这个错误并没有什么问题,因为 PHP 文档和生态系统很容易导致他们犯这个错误。 现在,PHP 臭名昭著的容易出错的问题甚至影响到非人类的生命。
此外,当提示使用 () 时,原始 GET 变量会愉快地传递到命令行。
有趣的是,当我添加一个只是 () 的函数(我决定将其命名为 ())时,它有时会记得在渲染数据库结果时通过此过滤器传递数据库结果。 但只是有时。
安全漏洞示例:差一
我会给你一个提示,让我们编写一个基本的监听器。 它对编写大量样板文件有很大帮助,而且编译也毫不费力。 然而,该函数在执行实际监听任务时会遇到基本的逐一缓冲区溢出错误。
打开和缓冲命令的函数
// a function that opens a socket and accepts commands into a buffer
int accept_commands(int sockfd) {
char buffer[1024];
int n;
while (1) {
n = read(sockfd, buffer, sizeof(buffer));
if (n < 0) {
perror("read");
exit(1);
}
if (n == 0) {
printf("connection closed\n");
exit(0);
}
buffer[n] = '\0';
printf("%s\n", buffer);
}
return 0;
}
如果缓冲区已满,[n] 可能会超出缓冲区末尾,从而导致 NUL 写入越界。 这个例子是一个很好的例子,说明像这样的小错误如何在 C 语言中像杂草一样生长,并在现实世界中被利用。 使用它的程序员可能会仅仅因为没有注意到差一问题而接受此代码。
总结
这三个易受攻击的代码示例并不是谎言,如果您直接询问它,它会很乐意编写执行该功能的代码。 不可避免的结论是:存在安全漏洞的代码可以而且经常会被编写,特别是当程序是用内存不安全的语言编写时。
擅长编写样板,但这些样板可能会妨碍程序开发人员找到好的部分; 还擅长猜测正确的常量、设置函数等。但是,依靠它来处理应用程序逻辑可能很快会让您误入歧途。 部分原因是,并不总是能够维护足够的上下文来正确编写多行代码,部分原因是互联网上的许多代码本身就容易受到攻击。 在这个模型中,专业人士编写的代码和初学者的家庭作业之间似乎没有系统的区别。 神经网络会做他们看到的事情。
请以合理的怀疑态度对待任何生成的应用程序逻辑。 作为代码审查者,我希望人们清楚地标记哪些代码是由 . 我不希望这种情况得到完全解决,这是生成模型如何工作的一个根本问题。 可能会继续进行增量改进,但只要它可以生成代码,它就会继续生成有缺陷的代码。
原文链接: