iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
AI Security

你該防的不是駭客,是你自己的 AI:在本機驗證你的防線系列 第 30

Day 30|AI 幫我寫洞,也幫我找洞,最後我只拿得出十九列證據

  • 分享至 

  • xImage
  •  

有人問我:所以你這個專案,現在安全嗎。

我答不出來。做了什麼我知道,但二十九天下來留了一堆東西,攤開來反而不知道該拿哪一份來回答。

今天就是把那堆東西收成一張表。昨天那條 XSS 資料流留了兩列給今天,等一下會用到;沒跟到昨天也不影響,需要的前情我在下面補。

先把五份加起來看看

手上有這些:

  • 攻擊集 22 條:該被擋下的輸入,每次改完防護的邏輯就整組再打一遍
  • 應放行集 5 條:不該被擋的正常輸入。防線把正常客人擋掉,那也是壞了
  • 安全回歸清單 16 列:修過的洞,每條綁著當初真的打穿它的那個輸入
  • 候選標注表 13 列:本機模型說「這個編號的問題在哪個檔」,我一條一條核對過的結果
  • 成分表五格:那一輪是拿哪一份權重、哪一版提示、哪一版腳本跑的

加起來六十一項。我第一個念頭是:乾脆算個通過率。我真的開始數,數到第三份就停了。

這三十天我其實在驗兩種東西。一種是 AI 幫我寫進去的洞補了沒有,另一種是 AI 幫我找洞找得準不準。那十三列記的是後者,那十六列記的是前者。模型指得準,不代表防線擋得住。反過來也一樣。

兩件事共用一個分母,兩個數字就都不能看了。

六十一那個分母,是我想要一個好看數字的時候才會成立的。

所以先定:什麼東西可以成為一列

這裡要分兩層,我第一版沒分,結果自己打自己:判準寫「三個條件缺一不可」,表上卻有一列沒有重跑指令。

第一層決定它進不進這張表:有沒有一條講得出來的路徑,跟一個可觀察的終點。那個終點要在沒打進去的時候不會亮,第 5 天那條是 window.__xss,第 24 天那幾條是「那筆訂單還在不在」。

會不會亮是重點,不是畫面上有沒有出現那串字。昨天那句講得更直接:字串裡出現它,只證明它被印出來了。

第二層決定它能不能簽過了或沒過:還要有一個具體的輸入,跟一條寫得下來的重跑指令。

第一層過了、第二層還不完整,那一列就留在表上,簽沒測。「整條鏈」放的正是這個位置:路徑跟終點說得出來,但還沒有具體輸入,也沒有重跑指令。

第一層都沒過的,連進表都不必。那兩份掃描核對紀錄就是這種:它們沒有攻擊輸入,也沒有一個「被擋下」的終點,量的是模型,不是防線。

判準檔長這樣,欄位都是人填的:

組	重跑	成分表	簽核
攻防案例	bash recipes/24-green-or-never-hit/run.sh	不適用	9/7/0
整條鏈	(沒有)	需要	0/0/1

「簽核」那三個數字是過了、沒過、沒測。腳本自己算一次,跟這一欄比。

那二十二條攻擊集加五條應放行集沒有消失,它們一起跑、一起判,所以整組算一列。表上的十九列是這樣湊出來的:十六條案例、那一組、加昨天那條鏈拆出來的兩列。

**判準不能從被評的那個東西的輸出反推,得先由人寫下來。**第 25 天那條紅線在這裡再走一次。

攤開來是十九列

  攻防案例	過了 9、沒過 7、沒測 0(簽核 9/7/0)
  輸入側回歸	過了 1、沒過 0、沒測 0(簽核 1/0/0)
  鏈的出口那半	過了 1、沒過 0、沒測 0(簽核 1/0/0)
  整條鏈	過了 0、沒過 0、沒測 1(簽核 0/0/1)

  合計 19 列:過了 11、沒過 7、沒測 1

那個 7 是第 24 天就在的七條已知缺口,第 25 天數過一次,今天還是七條。

先講清楚十九是什麼:**它不是十九條測試。**十六列逐條簽核,另有一列代表整組二十七個輸入,最後兩列是同一條資料流的兩個層級。這是一份簽核清單的長度,不是涵蓋數,拿去跟別的專案比大小沒有意義。

進表的四組流向過了沒過沒測,上面是沒進表的四項

這張圖要看的是上面那一塊。下面那四組都有路徑跟終點,所以流得到右邊那三個結果,其中「整條鏈」流到沒測是因為它還缺一條重跑指令;上面那三項量的是模型不是防線,最後一項是還沒有人替它寫案例。

簽核不是及格

我一開始想讓這支腳本在有東西沒過的時候回 1,這樣接進 CI 才擋得住人。七條缺口是每天都在的,那會變成每天叫一次的鬧鐘。

所以回 1 的條件改成跟簽核對不上

包括從沒過變成過了。有人把某條缺口補起來是好事,但那也是狀態變了,也要有人回去重新看一次再簽。不然過幾週你會有一張沒人記得為什麼長這樣的表。

真正要守死的是另一件事:不准把沒跑過的東西簽成過了。「整條鏈」那一列的重跑欄寫著「(沒有)」,簽核就只能是 0/0/1

反方向也要守,但不能守死。我第一版寫成「有重跑指令就不准簽沒測」。那條太硬。有一種簽沒測是合法的:那條指令這次跑不動。第 28 天那支在託管的機器上就是這樣,那份權重不在那裡。把它跟「寫了指令沒跑」判成同一件事,等於逼人刪掉還沒跑得起來的指令。所以檢查改成先看那條指令這次有沒有跑出結論,跑不動才准簽沒測。

那一欄叫「成分表」,而三組寫的是「不適用」

昨天那份成分表要在這裡兌現:每一列都得答得出「這個結果是拿什麼跑出來的」。所以表上多一欄,兩個值。「不適用」是這一組的判準不看模型說了什麼,那筆訂單還在不在、DOM 裡有沒有生出那個節點,換一份權重都不會變;「需要」是這一組要真的打一次模型,那就得記得住權重、提示、腳本各是哪一版。

填完之後這一欄自己講了一件事:四組裡三組寫「不適用」。

唯一寫「需要」的是「整條鏈」那一列,而它沒有重跑指令,簽的是沒測。所以成分表管得到的東西,這張表上一列都沒有,它管得到的第 26、27 天那兩輪全部在表外。

我本來以為這一欄只是回頭對帳,結果它把上面那個問題答完了。

沒進表的那幾項,跟一個我沒想到的數字

只列「測過的東西」的表,看起來跟「全部都測過」的表一模一樣。差別只在有沒有人把沒測的那些寫下來。

所以另外一份清單,一項一個理由:

  • 逐檔掃描那九列核對,量的是模型不是防線。而且那一輪跑的時候還沒有成分表,換一台機器連拿什麼跑的都答不出來
  • 給編號那十三列候選標注,一樣量的是模型。這輪有成分表,五格對得上代表那五樣沒變,但昨天講過那離重驗得回去還差一截
  • 成分表那五格,它是判準不是案例
  • 三個重跑入口都沒有提到檔名的那幾支檔

最後這一項是我寫檢查的時候才算出來的。

第 26、27 天掃的是範例專案底下那七支 .js,這個「七」不是我用 find 掃出來的,是第 28 天那份成分表裡登記的語料清單,因為那才是真的餵給模型的那一批。三個重跑入口所指的腳本裡,只有第 29 天那支提到 src/render.js

其餘六支,沒有一支的檔名出現在那三個重跑入口所指的腳本裡。

這句只到這個程度,就是一次靜態的檔名比對。它證不了執行時真的碰過或沒碰過,也可能只命中註解。要講「打過」得加上執行時的檔案存取追蹤,我沒有做。前面幾天的 recipe 有沒有順手碰到它們,我也沒有逐支去查。

掃描清單跟重跑入口提到的檔名不是同一批。這件事我做了三十天沒發現,是把兩份清單放在同一支腳本裡比才掉出來的。

檢查那一條不接受手寫的數字,它自己數一次再跟清單上的字比:

bash recipes/30-what-does-it-actually-cover/verify.sh 5
=== 5 掃描清單跟重跑入口提到的檔名不是同一批,差幾支是數出來的 ===
  通過	語料七支,重跑入口提到的 1 支,沒提到的六支

我把那份清單上的「六支」改成「三支」試過,它會沒過。

昨天那個懸案,答案是它不進這張表

CWE 是弱點類型的公開編號表,講「命令注入」「跨站腳本」這種類別。第 26 天起我一直拿它問模型「這一類的問題在哪個檔」,昨天結尾卡在一個問題:模型指的那兩條,行號跟來源判斷都可疑,那算過了還是算沒過。

拿第一層那兩個條件對一次就清楚了:沒有一條說得出來的攻擊路徑,也沒有一個「被擋下」的終點。它不是這張表的列。

順序不能反過來:先有「什麼可以成為一列」,才有「這一列算哪一欄」。

重跑入口:跑一次才發現有三支不在 CI 裡

一張不會自己重跑的表,過幾週就只是一份舊紀錄。所以我加了一條檢查:每一支腳本都要在 CI 的分級表上有一列。跑下去三個沒過:

=== 6 每一支 recipe 都有分級,重跑入口接得回 CI ===
  沒過	28-what-was-it-run-with 沒有分級,它不在 CI 的任何一個矩陣裡
  沒過	29-single-checks-combined 沒有分級,它不在 CI 的任何一個矩陣裡
  沒過	30-what-does-it-actually-cover 沒有分級,它不在 CI 的任何一個矩陣裡

前面兩支是我這兩天自己寫的,第三支是今天這份,寫完跑過、通過了,然後就沒有再想起它們。**跑得過跟被跑到是兩件事。**你自己那份表也會這樣:寫完那天你記得跑,第三週就只剩 CI 記得。

補完三列之後還有一句要寫進分級表。第 28 天那支找不到那份權重就回 2,這件事我在本機把路徑挪開驗過;託管的機器上不會有那份權重,所以它在那裡只會回 2。不寫下來的話,它在表上看起來就是過了。

要老實講這條檢查的範圍:它只確認每支都有登記,自己讀不到 CI 設定。真的去比對「表上說擋合併的,設定裡有沒有」的是第 22 天那支,兩條合起來才接得完。

換到你自己的專案

你手上大概一條驗過的攻擊都沒有。沒關係,這張表可以先從空的開始,重點是它從第一天就有兩個檔。

第一步,先開 uncovered.tsv,也就是沒測那份,你現在手上多半都是它。一項一個理由,最不安的那條就寫在這裡,「還沒想到怎麼測」也是理由。寫下來就好,總比過兩週你自己也忘了強。

第二步,從那份清單挑一條你想得出終點的,去打一次。判準沿用這三十天的:**那個終點在沒打進去的時候不會亮。**想不出終點就換一條,那條現在還不能驗。

第三步,打完的那條才進 standard.tsv,一列一條:輸入是什麼、終點看哪裡、重跑指令、你現在簽的是哪一格。打穿了就簽沒過,這一步最反直覺。它不在表上的話,你的表會全部都是過了,而那只證明你沒打到的地方沒有紀錄。

順序是這樣的:東西先在沒測那份出生,打過一次才搬家。反過來做的話,你會有一張漂亮的空表,跟一堆沒被寫下來的不安。

計數器:十九列的表、四項的沒測清單,前五份沒動

前面那五份的數字跟昨天一樣。

recipe 30 裡有九節檢查,要 nodejsdom。我也把每一節各弄壞一次跑過,包括把七條缺口簽成過了那一種。

第三十天

第一天那個問題很小:AI 三十秒幫我串好的那支 API,金鑰在誰手上。

三十天後我能誠實給的答案,是十九列加一份沒測清單,而不是一句「安全了」。

AI 幫我寫洞,也幫我找洞,這兩件事留下的是兩種證據。把它們加在一起算通過率,只會得到一個好看的錯答案。

比起第一天,多的是分得出「這條打過了」跟「這條沒打過」的能力。

那兩個檔今天就開得起來,先開沒測那份。你寫得出來的每一條不安,都比一張全部寫著過了的表老實。


上一篇:Day 29|模型找對兩個位置,卻沒串起同一條 XSS 資料流

今天這一份:recipe 30範例專案


上一篇
Day 29|模型找對兩個位置,卻沒串起同一條 XSS 資料流
系列文
你該防的不是駭客,是你自己的 AI:在本機驗證你的防線30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言