AI生成的程式碼你敢用嗎?有人給最近走紅的Copilot做了個「風險評估」

數據分析那些事
數據分析不是個事
14 min readAug 2, 2021

近日,GitHub 推出了一款利用人工智慧生成模型來合成程式碼的工具 — — Copilot,但釋出之後卻飽受爭議,包括版權爭議、奇葩註釋 和涉嫌抄襲。除此之外,生成的程式碼能不能用、敢不敢用也是一大問題。在這篇文章中,Copilot 測試受邀使用者 0xabad1dea 在試用該程式碼合成工具後發現了一些值得關注的安全問題,並以此為基礎寫了一份簡單的風險評估報告。

GitHub 真好,就算我因為 ICE 已經叨擾了他們好幾百次,他們還是給予了我進入 Copilot 測試階段的許可權。這次,我不關心 Copilot 的效率,只想測試它的安全性。我想知道,讓 AI 幫人寫程式碼風險有多高

每一行提交的程式碼都需要人來負責,AI 不應被用於「洗刷責任」。Copilot 是一種工具,工具要可靠才能用。木工不必擔心自己的錘子突然變壞,進而在建築物內造成結構性缺陷。同樣地,程式開發者也應對工具保有信心,而不必擔心「搬起石頭砸自己的腳」。

在 Twitter 上,我的一位關注者開玩笑說:「我已經迫不及待想用 Copilot 寫程式碼了,我想讓它寫一個用於驗證 JSON 網頁 token 的函式,然後看都不看就提交上去。」

我按照這一思路使用了 Copilot,得到的結果很是搞笑:

除了刪除硬碟驅動器之外,這可能是最糟糕的實現了。這種錯誤是如此明顯、粗陋,任何專業的程式開發者對此都不會有異議。我更感興趣的是 Copilot 是否會生成乍一看很合理的程式碼,以至於其中的錯誤會被程式設計師忽視或被經驗不足的程式設計師認為是正確的。(劇透一下:確實會出現這種情況。)

我有意使用 Copilot 生成實際應該人工編寫的程式碼,因為使用者肯定會在生產過程中這樣做。

不確定性

Copilot 是一種生成模型,這意味著其目的是生成在統計學意義上近似其輸入(即訓練資料)的輸出結果。其目標並不是精準重現其輸入,因為單純的復現並不需要機器學習系統。這類模型有一個通常被稱為「溫度(temperature)」的引數,這是一個滑動比例,可在保守風格(模仿最常見的輸入)和原創風格之間調整。原創性設定越高,輸出的結構化程度就越低,就越有可能得到垃圾結果。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。這兩個實現乍一看都是可行的,而如果你比較一些結果,你會發現很難說其中哪一個是正確的。

實際上,我在生成一個復活節日期計算器時多次得到了完全一樣的輸出結果,而且這個計算器是正確的(至少在我驗證過的一些年份上是正確的)。我猜想這意味著復活節計算器實現之間的偏差要比月相計算器之間的偏差小得多。

復活節計算器

可解讀性

上面的復活節計算器是正確的,但我也只是透過實驗知道的;它實在太難以解讀了。(更新:有人在評論區指出有一個書寫錯誤會影響少量年份 — — 這是逃過了我的檢驗的漏洞!)

Copilot 可以並且有時候肯定會增加註釋,但在這裡沒有影響。其中的變數名也完全毫無用處。我毫不懷疑其中一些是沒有明確名稱的中間結果,但整體而言,它能夠做到更加清晰。有時候,回到開始從註釋的起點開始呼叫,會讓 Copilot 試圖給出解釋。舉個例子,在函式中間提示 //f is 會讓 Copilot 宣告 // f is the day of the week (0=Sunday),但這似乎並不對,因為復活節星期日(Easter Sunday)往往是在星期日。其還會宣告 // Code from http://www.codeproject.com/Articles/1114/Easter-Calculator ,但這似乎並非一個真實網站連結。Copilot 生成的註釋有時候是正確的,但並不可靠。

我嘗試過一些與時間相關的函式,但僅有這個復活節計算器是正確的。Copilot 似乎很容易混淆不同型別的計算日期的數學公式。舉個例子,其生成的一個「格列高利曆到儒略曆」轉換器就是混雜在一起的計算星期幾的數學公式。即使是經驗豐富的程式設計師,也很難從統計學上相似的程式碼中正確辨別出轉換時間的數學公式。

金鑰以及其它機密資訊

真實的密碼學金鑰、API 金鑰、密碼等機密資訊永遠都不應該釋出在公開的程式碼庫中。GitHub 會主動掃描這些金鑰,如果檢測到它們,就會向程式碼庫持有者發出警告。我懷疑被這個掃描器檢測出的東西都被排除在 Copilot 模型之外,雖然這難以驗證,但當然是有益的。

這類資料的熵很高(希望如此),因此 Copilot 這樣的模型很難見過一次就完全記住它們。如果你嘗試透過提示生成它,那麼 Copilot 通常要麼會給出一個顯而易見的佔位符「1234」,要麼就會給出一串十六進位制字元 — — 這串字元乍看是隨機的,但基本上就是交替出現的 0–9 和 A-F。(不要刻意使用它來生成隨機數。它們的語法是結構化的,而且 Copilot 也可能向其他人建議同樣的數字。)但是,仍然有可能用 Copilot 恢復真實的金鑰,尤其是如果你使用十個而非一個建議開啟一個窗格時。舉個例子,它向我提供了金鑰 36f18357be4dbd77f050515c73fcf9f2,這個金鑰在 GitHub 上出現了大約 130 次,因為它曾被用於佈置家庭作業。任何在 GitHub 上出現過 100 次以上的東西都不可能是真正敏感的東西。最現實的風險是天真的程式設計師接收自動填充的密碼作為加密金鑰,這會讓所得到的值看起來隨機,但其熵卻很低很危險。

透過提示來生成密碼會得到各種有趣的不安全樣本。在訓練資料中,這些樣本通常是作為佔位字串使用的。大家最喜歡的佔位字串是「mongoose」。對一些使用者而言,生成髒話詞彙可能會造成一些問題。

證書清洗

GitHub 已經公開表示他們在 Copilot 模型中包含了託管於該網站的所有公開程式碼,並且不管證書如何。很明顯,他們認為這算是正當使用,不受制於證書限制,但這樣的意見在法庭上是否站得住腳…… 還有待觀察。

可以很容易驗證,Copilot 包含 GPL 程式碼,因為 Copilot 可以很容易從記憶中引用 GPL 證書文字。用 Copilot 寫出類似於某些具有獨特命名慣例的 GPL 專案的程式碼也很容易。

關鍵在於,Copilot 可用於「證書清洗」,做法是透過提示讓其對不想要證書下的程式碼進行細微的修改。對於使用 Copilot 的所有人而言,這有可能突然成為一個大的法律問題,也可能不會成為問題。

安全漏洞示例:用 C 寫的 HTML 解析器

一位朋友建議使用「具有正則表示式的通用 HTML 解析器」來為 Copilot 提供提示,這恰好是一個你不應該做的例子;Copilot 實際上拒絕使用正則表示式,而是編寫了一個完備的 C 函式和相當好的 main() 來驅動它。我做出的唯一修改是註釋掉 free(html),因為 free() 沒有透過 include 定義並且在任何情況下都不是必需的。

#include <stdio.h>// 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 <html file>\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,其 main 函式在開啟檔案時有一些很有用的樣板。但是,其解析過程有很多問題。

●首先,最重要的是,如果被解析的字串不包含 >,那麼解析器就會直接執行到緩衝區的末端並導致崩潰。
●這個解析器完全不清楚單獨的 < 和引用的 “>” 之間的區別,並會直接考慮首先出現的 >,而不會考慮其語法功能。
●屬性的定義是緊跟在右括號之後的非空白序列。舉個例子,在 <body>glory 中,glory 是一個屬性。屬性屬於它們的標籤內部,並且可以有多個以空格分隔的屬性。
●沒有任何報錯能力。

好訊息是其有數量驚人的設計精妙的指標數學,其工作起來就像是…… 之前已經設計好的。很難說這是預先設計好的,因為這個函式其實並沒有做什麼有用的事情,儘管它與基礎解析器的功能差不多有 80% 相似。當然,因為執行到緩衝區末端而直接引發的崩潰是一個致命的安全問題。

安全漏洞示例:用 PHP 寫的 SQL 注入

前兩行是我的提示。

這個樣板直接犯了大錯,產生了 2000 年代早期最典型的安全漏洞:PHP 指令碼採用原始的 GET 變數並將其插入到用作 SQL 查詢的字串中,從而導致 SQL 注入。對於 PHP 初學者來說,犯這樣的錯無可厚非,因為 PHP 文件和生態系統很容易導致他們犯這種錯誤。現在,PHP 那臭名昭著的容易誘導人出錯的問題甚至也對非人類生命產生了影響。

此外,當提示使用 shell_exec() 時,Copilot 很樂於將原始 GET 變數傳遞給命令列。

有趣的是,當我新增一個僅是 htmlspecialchars() 的 wrapper 的函式時(Copilot 決定將其命名為 xss_clean()),它有時候會記得在渲染資料庫結果時讓這些結果透過這個過濾器。但只是有時候。

安全漏洞示例:Off By One

我為 Copilot 給出提示,讓其寫一個基本的監聽 socket。其大有幫助地寫了大量樣板,並且編譯也毫不費力。但是,這個函式在執行實際的監聽任務時會出現基本的 off-by-one 緩衝溢位錯誤。

一個開啟 socket 並將命令收入緩衝區的函式

如果緩衝區填滿,buffer[n] 可能指向超過緩衝區末端之後再一個,這會導致超出邊界的 NUL 寫入。這個例子很好地表明:這類小漏洞在 C 中會如野草般生長,在實際情況下它是有可能被利用的。對於使用 Copilot 的程式設計師而言,因為未注意到 off-by-one 問題而接受這種程式碼還是有可能的。

總結

這三個有漏洞的程式碼示例可不是騙人的,只要直接請求它寫出執行功能的程式碼,Copilot 就很樂意寫出它們。不可避免的結論是:Copilot 可以而且將會常常寫出有安全漏洞的程式碼,尤其是使用對記憶體不安全的語言編寫程式時。

Copilot 擅於編寫樣板,但這些樣板可能阻礙程式開發人員找到好的部分;Copilot 也能很準確地猜測正確的常數和設定函式等等。但是,如果依賴 Copilot 來處理應用邏輯,可能很快就會誤入歧途。對此,部分原因是 Copilot 並不能總是維持足夠的上下文來正確編寫連綿多行的程式碼,另一部分原因是 GitHub 上有許多程式碼本身就存在漏洞。在該模型中,專業人員編寫的程式碼與初學者的家庭作業之間似乎並沒有系統性的區分。神經網路看到什麼就會做什麼。

請以合理質疑的態度對待 Copilot 生成的任何應用邏輯。作為一位程式碼審查員,我希望人們能清楚地標記出哪些程式碼是由 Copilot 生成的。我預期這種情況無法完全解決,這是生成模型工作方式的基本問題。Copilot 可能還將繼續逐步改進,但只要它能夠生成程式碼,它就會繼續生成有缺陷的程式碼。

原文連結:https://gist.github.com/0xabad1dea/be18e11beb2e12433d93475d72016902

※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※

我是「數據分析那些事」。常年分享數據分析乾貨,不定期分享好用的職場技能工具。各位也可以關注我的Facebook,按讚我的臉書並私訊「10」,送你十週入門數據分析電子書唷!期待你與我互動起來~

文章推薦

資料視覺化,必須注意的30個小技巧!

這一份最全的TCP總結,請務必收下

八個行業30個免費資料網站來源,從此不愁找資料了

--

--

數據分析那些事
數據分析不是個事

這是一個專注於數據分析職場的內容部落格,聚焦一批數據分析愛好者,在這裡,我會分享數據分析相關知識點推送、(工具/書籍)等推薦、職場心得、熱點資訊剖析以及資源大盤點,希望同樣熱愛數據的我們一同進步! 臉書會有更多互動喔:https://www.facebook.com/shujvfenxi/