一張測試號在生產庫裡躺了三週
有個訂閱制下單頁,月底對帳翻出幾筆「格式完全合法」卻刷不出錢的訂單,卡號一欄是 4242 4242 4242 4242——境外支付網關公開文件裡的通用測試號,當年被測試同事拿來跑通前端流程,然後就順著同一套校驗留在了生產庫裡。
那套前端只做了一件事:Luhn(模 10)求和看餘數。函式沒寫錯,出事的是「通過」被讀成「這張卡是真實的」。4242424242424242 在校驗頁裡拿到的是「Luhn 校驗通過」:卡組織 Visa(維薩)、長度 16 位符合慣例、BIN 424242、校驗位 2、校驗和 80 除以 10 餘 0——沒有一個字提示它是測試號。
Luhn 只對「一位打字錯」負責
規則就一句:從右往左數,偶數位數字乘 2,乘積大於 9 時減 9,其餘位不變,全部相加看能否被 10 整除。被乘 2 的位走的是映射 f:0→0、1→2、2→4、3→6、4→8、5→1、6→3、7→5、8→7、9→9。16 位卡上被乘 2 的是自左第 1、3、5、7、9、11、13、15 位,一個「42」對的貢獻正好是 4→8 加 2→2 得 10,8 對就是 80,恆被 10 整除。放它過去靠的是算術不是名單:模板出現 4242 的次數是 0,8 個按鈕裡也沒有它。
往下推:「42」重複 5 次到 9 次得到 10 位、12 位、14 位、16 位、18 位,校驗和依序是 50、60、70、80、90,全部整除 10,只有 10 位那條先被「長度 12–19」擋下。放寬到任意「ab 重複 8 次」,只要加倍位的映射值加非加倍位是 5 的倍數就通過,這樣的組合共 20 個,00 與 88 被全同守衛擋下。
圖 1|同一對「42」每多重複一次,校驗和就加 10。
它確實擅長的那件事:抓單個字元打錯
Luhn 留在前端是有道理的:把 7 張示範/測試卡的每一位都換成另外 9 個數字,窮舉的 1008 個單字元變體漏檢 0 個(檢出率 100.00%);算上 4242 4242 4242 4242,8 張卡口徑的 1152 個變體也不漏;只改末位,63 個變體也全被抓到。
對調兩位,它有兩種徹底看不見的方式
麻煩從順序顛倒開始:16 位卡帳號位之間的相鄰對調窮舉 1260 組,漏 28 組(2.2%),漏的全是 0 與 9 互換。第二種才是大洞:對調的兩個槽位「是否被乘 2」狀態相同(16 位卡上即距離為偶數)時,兩邊各拿回同一個權重,校驗和不變,永遠檢不出——這類 4410 組漏檢 4410 組,100.00%。一例:4532015112830366 第 3 位與第 5 位互換得 4502315112830366,兩號都通過,校驗和 50→50。
兩位同時抄錯(各自換數字):16 位卡 9720 組漏 1080 組,檢出 88.89%;15 位卡 8505 組漏 945 組,漏檢率同為 11.11%。那句「約 99% 的雙字元錯誤」在這個口徑下夠不到。
圖 2|16 位卡上五類錄入錯誤的漏檢量,條形按漏檢率等比。
隨手編一個號,也有約 1/10 過關
表單要防的第二類人不是抄錯的收銀員,是隨手編號的人。固定種子生成的 10,000 個隨機 16 位號裡 Luhn 通過 987 個(9.87%);把 BIN 釘死在 424242 再窮舉剩下的 1,000,000 種組合,正好 100000 個通過(10.00%),卡組織一欄全是 Visa(維薩)。1/10 出自模 10 本身,不是抽樣;25 條公開測試號裡 19 條本身就通過,含 4000000000000002。
「卡組織:Visa」這行字撐不起風控結論
結構表容易被讀成一份登記冊,實情是 7 條前綴正則加一張長度表:6 位 BIN 的 1,000,000 個取值裡只有 288300 個(28.83%)能歸到 7 家之一,剩下 711700 個(71.17%)落進「未知(不在常見號段表內)」,BIN 那行只是前 6 位拼一句固定文案「用於確定發卡機構與卡種」。前綴表也比文案寬:萬事達那條收進了 2221–2720 之外的段,2799999999999999 一樣判 Mastercard(萬事達)。長度欄是白名單:7 家 len 陣列元素總數只有 15 個,長度 12 誰都不認,411111111117 被放行卻提示「與慣例不符」。
| 表單想問的 | 實際能答的 | 實測 |
|---|---|---|
| 抄錯一位 | 能,唯一可靠的一項 | 1008 個變體漏檢 0 個 |
| 兩位順序打亂 | 同奇偶槽位永遠不能 | 4410 組全漏(100.00%) |
| 卡是真的嗎 | 完全無關 | 隨機號 987/10000 通過 |
| 哪家銀行發的 | 只到卡組織一級 | 711700 個 BIN 判不出 |
「末位應為 X」永遠唯一,卻常常把號改錯
不通過分支必給一句「若前 15 位錄入無誤,末位應為 X」。數學上沒錯:前段固定時使校驗和整除 10 的數字恰好只有一個,100,000 組隨機前段裡解個數為 1 的 100000 組。錯的是那句前提——5 張卡的 738 個單字元錯裡末位真錯只有 45 個,其餘 693 個錯在中間位,照建議改末位能回到原號的 0 個,可 738 個改完全都通過,還有 66 例順帶改掉卡組織標籤。這是把錯號換成另一張合法號,不是修復。
分層,以及這套校驗給不了的邊界
分工應該很清楚:Luhn 留在前端只當打字錯誤的煞車,結構核對只到卡組織一級,判真偽的是網關授權那一步——測試號只在沙箱環境有效。想把號碼逐位攤開對照著看,我用銀行卡號校驗;身份證那套是加權求和模 11,與這裡「偶數位乘 2、模 10」不同族,可對照身份證號碼校驗與身份證校驗位。
邊界也要說清:單次只處理一個卡號,模板裡 <input> 只有 1 處、9 個按鈕、<textarea> 與檔案入口 0 處;有效期與安全碼欄位命中 0 次。腳本層 fetch、sendBeacon、localStorage 命中 0 項,卡號不離開瀏覽器,但頁面殼層仍要載入樣式與統計腳本——「不上傳卡號」和「不發任何請求」是兩件事。
Q:Luhn 通過能不能說明這張卡是真的?
不能。通過只等於校驗和整除 10:4242424242424242 通過(校驗和 80),隨機編的號也有 987/10000 通過。它是糾錯碼,不是發卡行資料,也不是授權。
Q:能查出是哪家銀行、哪個卡種嗎?
查不到。BIN 那行只輸出前 6 位拼一句固定文案,表裡沒有發卡行資料;6 位 BIN 裡 711700 個連卡組織都判不出。
Q:能順手校驗有效期和安全碼嗎?
不能,也不能一次跑多張:有效期、CVC、CVN、CID 關鍵詞命中 0 次,安全碼不在卡號本體裡,任何網頁都校驗不了它。