把预算上限写进代码,而不是写进自觉
脚本要每天自己往外发请求,最危险的不是发错,而是「今天已经发了多少」这件事只记在人的脑子里。所以把额度做成代码里的硬约束:一个 JSON 记账文件加四个检查点,超限直接拒发并打印原因。
一、记账结构
// state/budget.json:每发一个包立刻 charge + 落盘,不留批处理的余地
{"day": "2026-10-10", "total": 100,
"hosts": {"a.example.com": 10, "b.example.com": 15},
"last": 1791610072.6045036}
def budget_ok(st, host):
if st["total"] >= MAX_DAY: return False, "全天已到 %d 上限" % MAX_DAY
if st["hosts"].get(host, 0) >= MAX_HOST:
return False, "单主机 %s 已到 %d 上限" % (host, MAX_HOST)
return True, ""
# 跨天归零:day 不是今天就把整份计数丢掉,而不是去减
if st.get("day") != date.today().isoformat():
st = {"day": date.today().isoformat(), "total": 0, "hosts": {}, "last": 0}两个容易忽略的点:一是 charge 之后必须马上 save_state,否则脚本中途崩掉,下一轮又从 0 开始计数;二是跨天要整份归零,按时间衰减的写法在长时间停跑之后会算出负数或半满的幽灵额度。
二、白名单只在最终 URL 的 host 上判一次
如果按「URL 的书写形态」分支放行(带协议的走 A 逻辑、相对的走 B 逻辑),一定会漏。正确做法是解析成最终绝对地址之后,只在 host 上判一次,并且比较时吃点边界:
def inscope(url):
host = urlparse(url).netloc.split("@")[-1].split(":")[0].lower()
for d in ALLOWED: # d = "example.com"
if host == d or host.endswith("." + d):
return True, host
return False, host # 只有整段相等或以 .d 结尾才算只用 host.endswith(d) 会让 evil-example.com 混进来;顺手再把内网地址(10./172.16-31./192.168.)与保留段拦掉,成本三行。
三、GET 不等于安全
从前端打包产物里扒接口清单时,方法标注是不可信的:确实存在 {method: "get", url: "/api/delRule"} 这种「用 GET 执行删除」的写法。所以判定要叠一层名字语义:删除、绑定、支付、下发验证码、发消息这类写语义的路径,无论标注什么方法都不发。同理,能触发短信/邮件的接口要单独排除,因为它消耗的是真实第三方资源。
四、命中即停,以及「不要有两份守卫」
脚本里除了额度,还要有「出现疑似真数据立刻停」的分支:继续扩面会把一次探测变成一轮扫描。最后一条最工程化:守卫只允许一份实现,其它脚本 from guard import inscope, budget_ok,复制粘贴的第二份守卫一定会和第一份漂移,而漂移的方向通常是放松。
小结
四条失效写法对应四个事故:批处理落盘(崩了就归零)、按形态分支放行(漏 host)、信任方法标注(发出删除)、复制守卫(逻辑漂移)。把上限写进代码之后,「今天别发太多」这件事就不再需要人记得。
- 点赞
- 收藏
- 关注作者
评论(0)