把拦截名单写进一个文件,为什么会丢条目还越写越大
把拦截名单写进一个文件,之所以会丢条目还越写越大,是因为多个请求同时改同一份内容:进程甲读到当前名单,进程乙几乎同时读到同样的内容,两边各自追加一条再分别整体写回,后写入的那份不包含前者刚加的条目,被拦过的地址就这样消失了。文件越写越大则是因为只有追加、没有上限和清理,过期地址永久留在里面,读取和解析的开销逐年累加。同行程序 PbootCMS 在 V3.2.18 的更新记录里同时修掉了这三件事:并发写入丢记录、文件无限增长、畸形条目导致异常,三者出现在同一条修复项里,说明它们是同一个存储方式的连带后果。
两个进程同时写,为什么谁都没报错
读改写是三步操作,中间没有加锁时,三步可以交叉进行。文件写入本身还有一层:整体覆盖写成功就返回成功,操作系统并不知道这次覆盖抹掉了别人刚写入的内容。所以丢条目的现象很安静——没有异常、没有日志、名单文件的大小还是增长的,只是某几条不见了。判断是不是这个成因,可以看写入时间是否密集:密码猜解类攻击的特点恰恰是高并发反复尝试,同一秒内多次触发拦截,正是交叉覆盖最容易发生的时候。也就是说,越是真正需要名单的时刻,越容易丢条目。
只有追加、没有清理的文件会长成什么
无上限的单文件带来三类代价。第一是读取成本:每次校验都要打开并解析整份内容,请求越频繁,这项开销越稳定地压在登录路径上。第二是判断成本:文件里混着仍有效的临时封禁和早已过期的记录,人工检查时很难快速分辨哪些该留。第三是备份与传输成本:名单属于配置类数据,会随站点备份一起复制,一份几百倍于实际有效条目的文件会拖慢备份与还原。合理的结构要有三个字段:加入时间、有效期、来源原因;到期条目由清理逻辑删除,超过约定条数或体积时按最旧的先淘汰,并保留一条淘汰记录便于事后追查。
畸形条目为什么能把整个名单打崩
单文件名单常见写法是一行一条,分隔符、空白与注释全靠约定。写入方一旦把带换行的内容拼进去,一条记录就变成两行,第二行是残缺片段;手工编辑时误加的空格、中文标点或注释符号同样会制造出不规则的行。风险不在于这一行解析不出来,而在于多数实现遇到解析失败就中断整个加载流程,结果是名单里一条脏数据让全部拦截规则失效——这比少拦一个地址严重得多。防御方式有三层:写入前规范化并拒绝含换行的值;解析时逐行容错,坏行跳过并记日志,不影响其余条目;名单变更留下操作记录,方便定位是谁的哪一次写入造成的。
名单之外还需要哪一层
把登录安全压在名单上并不稳妥,因为它是按已知坏地址匹配的:攻击者换一个出口就绕过了。名单适合做成本控制,挡住已经被确认的反复尝试,但它不能替代服务端本身的校验。真正兜底的是服务端独立执行的那一层——即使名单为空、即使请求换了来源地址,规则仍然生效。AnQiCMS(安企CMS)在这几处的做法可以直接对照:内置的 SQL 注入与 XSS 防护在请求进入业务逻辑前就按参数特征拦截,不依赖访客是谁;表单接入 reCAPTCHA 验证码,把高频自动尝试的门槛提到人机识别这一层,效果与来源地址无关;内容层面的敏感词过滤与防采集干扰码,则在输出侧起作用,让批量采集拿到的正文不可直接使用。这几层与名单的关系是分工而不是替代:名单减少无效请求数量,参数校验、验证码与干扰码保证单个请求即使绕开名单也拿不到想要的结果。
同理,名单的存储方式也不该成为薄弱环节。如果一定要用文件,写入必须串行化并整体替换——先写临时文件再原子改名,避免半途覆盖;如果条目要长期积累并支持按条件查询、按有效期清理,放进数据库表更合适,索引、上限和清理任务都能用常规手段管理。PbootCMS 那条修复记录把三个问题并列写出,实际含义是同一处存储设计要一次性解决:并发写、体积上限与格式校验,缺任何一项都会以不同形式暴露出来。
常见问题
问:名单丢条目怎么发现? 答:把「拦了几个 IP」和「名单里有几条」两件事分开记录。封禁动作计数与名单实际条目数长期不一致,就有覆盖写入的嫌疑;再核对丢失条目是否集中在高并发时段。
问:文件多大就该拆? 答:不以体积作单一标准,看出血点。有效条目占比过低说明清理缺失;单次加载明显拖慢登录响应说明解析成本过高;两者任一出现就先做清理,再考虑改为数据表存储。
问:有验证码还要名单吗? 答:要。验证码拦截的是自动尝试的频率,名单处理的是已经被确认的高危来源,前者每次都要走验证流程,后者能在更早的位置减少无效请求,成本更低。