有人問我:所以你這個專案,現在安全嗎。
我答不出來。做了什麼我知道,但二十九天下來留了一堆東西,攤開來反而不知道該拿哪一份來回答。
今天就是把那堆東西收成一張表。昨天那條 XSS 資料流留了兩列給今天,等一下會用到;沒跟到昨天也不影響,需要的前情我在下面補。
手上有這些:
加起來六十一項。我第一個念頭是:乾脆算個通過率。我真的開始數,數到第三份就停了。
這三十天我其實在驗兩種東西。一種是 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 的分級表上有一列。跑下去三個沒過:
=== 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 裡有九節檢查,要 node 跟 jsdom。我也把每一節各弄壞一次跑過,包括把七條缺口簽成過了那一種。
第一天那個問題很小:AI 三十秒幫我串好的那支 API,金鑰在誰手上。
三十天後我能誠實給的答案,是十九列加一份沒測清單,而不是一句「安全了」。
AI 幫我寫洞,也幫我找洞,這兩件事留下的是兩種證據。把它們加在一起算通過率,只會得到一個好看的錯答案。
比起第一天,多的是分得出「這條打過了」跟「這條沒打過」的能力。
那兩個檔今天就開得起來,先開沒測那份。你寫得出來的每一條不安,都比一張全部寫著過了的表老實。
上一篇:Day 29|模型找對兩個位置,卻沒串起同一條 XSS 資料流