new-api 批量注册刷号排查与处置 (Investigating Bulk Fake Registrations)
场景
自建 new-api 站点开放注册赠送额度(QuotaForNewUser)后,被脚本批量注册账号、白拿额度并转卖或自用。站点同时暴露在公网,攻击者可能绕过 nginx 直打应用端口。
症状
- 用户数在一天内异常增长(本例一天 +177)
- 用户名呈现明显的机器生成特征:英文名 + 英文姓 + 数字(
DonaldWatson2004、RussellOrtiz161) - 这些账号的注册时间间隔高度规律(本例每 20–25 秒一个)
排查路径
1. 先看注册时间分布,确认是异常而非正常增长
select date(created_at,'unixepoch','+8 hours') d, count(*) from users group by d order by d;2. audit_logs 表是定位攻击手法最快的地方
new-api 的审计日志记录了登录、建 key、改配置等动作。批量刷号的脚本会留下固定的三步曲:
select created_at, username, ip, action, content from audit_logs order by id desc limit 30;典型模式:同一个 IP,每个新号在约 5 秒内依次发出
POST /api/user/login— 登录POST /api/token/— 建 API key,名字是脚本自动生成的(本例rt-<epoch秒>)POST /api/token/batch/keys— 把 key 明文取走
三条记录的 user_agent 都只写 Mozilla/5.0(脚本默认值)。
3. 注册请求本身在 journald,不在 audit_logs
journalctl -u new-api --since "2026-09-23 00:00" --until "2026-09-24 00:00" | grep "user/register"看三样东西:来源 IP、User-Agent、HTTP 版本。脚本常用 HTTP/1.0(浏览器用 1.1)。
4. nginx 访问日志用来区分「走 nginx」还是「直连应用端口」
grep "user/register" /www/wwwlogs/<域名>.log某 IP 出现在 nginx 日志里 = 走了 nginx;只看得到应用日志、nginx 里没有 = 直连了应用端口。
5. 算清损失
-- 每个新号送了多少
select value from options where key='QuotaForNewUser';
-- 实际被消耗了多少(type=2 是消耗流水)
select sum(quota) from logs where type=2;
-- 烧在哪些模型上
select model_name, count(*), sum(quota) from logs where type=2 group by model_name order by 3 desc;额度换钱:$1 = QuotaPerUnit = 500000。本例 sum(quota)=238577256 → $476.76。
处置(按有效性排序)
| 措施 | 作用 |
|---|---|
配 SMTP 并开 EmailVerificationEnabled | 最有效。每个号需要真实可用邮箱,把成本从"零"抬到"一个邮箱" |
降/关 QuotaForNewUser | 直接降低单个号的收益 |
EmailDomainRestrictionEnabled + EmailDomainWhitelist | 白名单机制,临时邮箱天然被挡。只放行名单内域名,攻击者换新临时域名也进不来——比维护"临时邮箱黑名单"强 |
EmailAliasRestrictionEnabled | 堵掉 Gmail/Outlook 的 + 别名一邮多号;代价是连 john.smith@ 这种带点的正常地址也会被挡 |
nginx deny <IP>; | 挡已知攻击 IP,快但被动 |
| nginx UA 风控 | 挡"懒得改 UA"的脚本,是减速带不是墙 |
Cloudflare Turnstile(TurnstileCheckEnabled) | 注册前加人机验证,需要 CF 的 Site Key / Secret Key |
nginx UA 风控写法(放在 server 块内)
# 风控:拦截脚本默认 UA(真实浏览器 UA 都带 "(系统…)" 等后缀)
location ~ ^/api/(user/register|verification)$ {
if ($http_user_agent = "Mozilla/5.0") { return 403; }
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}accept 用精确相等匹配 $http_user_agent = "Mozilla/5.0",浏览器完整的 UA 串不会命中。if + return 是 nginx 里少数安全的 if 用法。
删除批量账号(对齐 new-api HardDelete 语义)
先备份库,再删。HardDelete 删的是用户 + 认证数据,logs 保留:
delete from two_fa_backup_codes where user_id in (...);
delete from two_fas where user_id in (...);
delete from user_sessions where user_id in (...);
delete from auth_flows where user_id in (...);
delete from passkey_credentials where user_id in (...);
delete from tokens where user_id in (...);
delete from user_oauth_bindings where user_id in (...);
delete from users where id in (...);(源码:model/user.go 的 HardDelete + deleteUserAuthenticationData。)
坑
1. ApiError 返回的是 HTTP 200,日志里的 200 不代表成功
common/gin.go:
func ApiError(c *gin.Context, err error) {
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
}GIN 访问日志里 POST /api/user/register ... 200 可能只是"参数错误"。判断成败必须看响应体的 success 字段或响应长度。
2. EmailDomainWhitelist 是整个域名精确匹配
service/email_binding.go 用 slices.Contains(common.EmailDomainWhitelist, parts[1])。写 outlook 不等于 outlook.com,等于永远不放行。域名必须写全。
3. 改 options 表后要等同步周期(默认 60 秒)
common/init.go 的 SyncFrequency = GetEnvOrDefault("SYNC_FREQUENCY", 60)。改完立刻测会失败,日志里能看到 [SYS] ... | syncing options from database。
4. new-api 默认监听所有网卡,端口一旦暴露,nginx 上的所有防护全部失效
main.go:
srv := &http.Server{ Addr: ":" + port, Handler: server }Addr 是 ":" + port,绑定 0.0.0.0/::。只要这个端口在 NAT/安全组里开着,攻击者就能 http://<IP>:<PORT>/api/user/register 直接打,绕过 nginx 的 IP 黑名单和 UA 风控。
所以排查刷号时,必须先回答一个问题:应用端口有没有对公网开着?
5. 本机开着 Clash/代理时,/dev/tcp 端口测试全部是假阳性
Clash 的 TUN 模式会在本地先把 TCP 连接接住,再决定怎么路由。结果是测试任何 IP 的任何端口都"连接成功"——连明确关闭的端口也"通"。
本例实测:本机测 IP:3000 和 IP:12345 都显示"通",而 :12345 根本没有任何服务。
判据:看连接来源地址,如果是 198.18.0.1 这类保留段,说明流量被 TUN 接管,结论作废。
正确做法:从一台干净的第三方机器测(本例用 ssh tx 执行 curl --noproxy '*'),或者先用一个已知关闭的端口做校准。
验证方法
# 1) 脚本 UA 应被挡
curl -s -k -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0" \
-H "Host: <域名>" https://127.0.0.1/api/user/register # 期望 403
# 2) 浏览器 UA 必须放行
curl -s -k -o /dev/null -w "%{http_code}\n" \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36" \
-H "Host: <域名>" https://127.0.0.1/api/user/register # 期望非 403
# 3) 其它接口不受影响
curl -s -k -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0" \
-H "Host: <域名>" https://127.0.0.1/api/status # 期望 200
# 4) 改 nginx 后
nginx -t && nginx -s reload改完 UA 规则后,还要确认换任何 UA 应用返回都一样,排除"规则误伤":
for ua in "curl/8.16.0" "python-requests/2.31" "Go-http-client/1.1" "<浏览器UA>"; do
curl -s -k -o /dev/null -w "$ua -> %{http_code}\n" -A "$ua" -H "Host: <域名>" \
-X POST https://127.0.0.1/api/user/register
done # 应当全一致适用边界
- 针对 new-api v1.0.0-rc.40(
QuantumNous/new-api);HardDelete覆盖的表和选项名可能随版本变化。 - nginx 部分适用于宝塔面板管理的 nginx(配置目录
/www/server/panel/vhost/nginx/,重载用/www/server/nginx/sbin/nginx -s reload,systemctl reload nginx在这类机器上常不生效)。 - UA 精确匹配只能挡固定 UA 的脚本;攻击者改一个字母就绕过。真正的门槛是邮箱验证与人机验证。
本次实例(6767 API / la,2026-09-23 攻击、2026-09-24 处置)
- 攻击:
8.219.150.152等 IP 用脚本刷POST /api/user/register,一天 1470 次尝试,建成 177 个号;每个号自动领 $5,共白送 **$885;实际跑掉 $476.76**(烧在gemini-pro-agent$376、claude-opus-4-6-thinking$60)。 - 处置:配 QQ 邮箱 SMTP + 开邮箱验证;
QuotaForNewUser从 2500000 降到 250000(后按用户要求恢复为 2500000);block_ips.conf加deny;两个 vhost 加 UA 风控;删除 176 个批量号(含 172 个 token、174 个会话),库备份在/root/one-api.db.bak-before-purge-*。 - 未解:服务商 NAT 面板里 3000 端口的映射现状需人工确认(从 tx 实测 3000 对外不通);
EmailAliasRestrictionEnabled与TurnstileCheckEnabled仍未启用。
关联
- new-api 邮箱注册与 SMTP 配置 (Email Registration and SMTP Setup)
- new-api 前端定制与重编译部署 (Frontend Customization and Rebuild Deploy)
- new-api 批量生成兑换码 (Batch Generate Redemption Codes)
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps