一张测试号在生产库里躺了三周
有个订阅制下单页,月底对账翻出几笔「格式完全合法」却刷不出钱的订单,卡号一栏是 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 次,安全码不在卡号本体里,任何网页都校验不了它。