用户资料页允许填写个人主页,运营发现有人用短链把人引到抽奖页面。人工点开有风险,不点又无法判断,最后只能一刀切禁掉所有外链,正常用户也跟着受影响。
示例接口来自数据鱼API的「恶意 URL 检测」服务,本文关注其中的「单 URL 检测」分支(
POST /v1/api/url/scan)。检测单个 URL。
这类场景适合让接口先给一个可解释的分数。分数不是结论,但它能把“可疑”拆成几条具体的信号,运营和开发都能看懂。
单 URL 检测接口 POST /v1/api/url/scan 返回的 data.score 是 0 到 100 的整数,由各条信号权重相加后封顶。data.level 按分数分段:80 及以上是 block,50 及以上是 high,20 及以上是 medium,其余是 low。data.is_malicious 只有在分数达到 80 时才是 true。
data.signals[] 里每一项包含 name、weight 和 detail。举例来说,黑名单命中可能记 50 分,可疑顶级域记 10 分,钓鱼关键词记 12 分,域名里连字符过多记 8 分,四项加起来正好 80,进入 block。这个结构的好处是,即使总分不高,也能看到是哪几项在起作用。
data.categories 给出风险分类,常见值有 phishing、brand_impersonation、malicious_host、invalid_form、reserved_ip。分类适合做路由:钓鱼类进人工审核,仿冒类通知品牌方,内网地址类直接走另一套逻辑。
url 是必填参数,最长 2048 个字符。超过长度的链接应该在入口处就拒绝或截断,不要等接口报错。短链、带中文参数的链接、带端口的地址都可以直接提交,返回里的 data.url_normalized 是规范化后的结果。
check_ip_risk 默认 true,表示联动 IP 风险查询。设为 false 会关闭 L4 层,响应更快,但少了一层信号,分数可能偏低。对实时性要求很高的场景可以关闭,事后复核时再补一次完整检测。
data.host、data.scheme、data.port、data.path、data.query 是把链接拆开后的结果,适合做展示和日志归类。data.is_internal_ip 表示主机是否解析到内网或保留地址,这类链接不应该被当作普通外链处理。
data.ip_risk 是条件返回的:主机是内网或保留地址、没有命中 IP 风险库、或者关闭了联动时,这个键不会出现。里面包含 ip、is_risky、tags、risk_score、hit_count 等信息,severity_num 为 4 时表示解析出的 IP 命中了内网或保留段。
data.duration_ms 是本次检测耗时,可以用它来设置调用方的超时和熔断阈值。
is_malicious 为 false 不代表安全。分数 50 到 79 属于 high,虽然没到拦截线,仍然值得人工复核或加验证。只看布尔值,会把大量灰产链接放过去。
单条信号权重很高时,要结合 detail 看原因。黑名单命中通常证据权重更高;关键词、连字符、可疑顶级域属于启发式信号,单独出现时误报率会高一些。把 weight 排序后再决定处理优先级,比只看总分更细。
url_normalized 适合做去重和缓存键。同一个地址的大小写、默认端口、末尾斜杠写法不同,规范化后可能一致,用原始字符串做缓存会重复检测。
ip_risk 可能不存在。代码里先判断键是否存在,再取值,不要默认它一定返回。
内网地址不要直接做服务端抓取。is_internal_ip 为 true 时,链接指向的是内网或保留段,继续请求会带来 SSRF 风险,应该直接拒绝或转人工。
关闭 check_ip_risk 后分数会变。同一批链接在两种配置下结果不同,做趋势对比时要固定配置,否则数据没有可比性。
def scan_url(url: str):
resp = requests.post(
f"{API_BASE}/v1/api/url/scan",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"url": url[:2048], "check_ip_risk": True},
timeout=8,
)
data = resp.json()["data"]
if data.get("is_internal_ip"):
return {"action": "reject", "reason": "internal_ip"}
if data.get("is_malicious"):
return {"action": "block", "score": data["score"],
"categories": data.get("categories", [])}
if data.get("level") == "high":
return {"action": "review", "score": data["score"],
"signals": [s["name"] for s in data.get("signals", [])]}
return {"action": "allow", "score": data["score"]}风险分的价值在于可解释。把 signals 和 categories 一起落到日志里,误报时才有依据去调整策略,而不是拍脑袋改阈值。
本文只讨论数据鱼API「恶意 URL 检测」接口的接入方式,具体行为以官方文档为准。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。