测试时间:2026 年 1 月
测试对象:[某APP]
测试类型:内容风控 / 审核机制
测试人:0x34

0x01 先从一个头像开始

最开始其实并没有没想太多。由于当时想表现自己的与众不同,先拿了一张普通的人物图片,在衣服的位置加了一段比较明显的敏感英文:

porn

果断上传,结果发现头像显示保存成功,在我点开查看的时候,很快被换回去了,而且朋友那里也看不到。

这激发了我很大的好奇,因为头像这里不是完全没有审核,而且图片里的文字也是能被检测的到的。

于是我换了一张,这一次稳妥起见,没有在图片里添加任何敏感文字,但是也是使用了一张比较不雅,甚至有点猎奇的图片。结果发现并没有被拦截,结果跟前面一点也不通,头像成功上传了。

重新进入个人主页,头像已经换成了刚才上传的图片,我又让朋友从他的账号查看,头像也能够正常显示。

到这里,其实没有什么特别值得说的,因为至少从黑盒表现来看,平台能够识别出这两类内容,并在头像上传这个环节进行处理。于是我开启了第三次测试,使用两个具有明显政治敏感属性的旗帜图片。结果第一个旗帜被拦截,第二个旗帜成功上传。更离奇的是,不仅自己的头像显示正常,换一个账号查看时,也能正常看到这张图片。

也就是说,单纯从最终结果来看:

测试样本

结果

明显裸露色情图片

拦截

某类具有明显政治敏感属性的旗帜图片

拦截

另一类具有明显政治敏感属性的旗帜图片

正常上传并显示

这时候问题就不再是简单的“这个 APP 有没有审核”,答案很明显:它显然是有审核的。

真正让我开始往下想的是:为什么同样属于敏感内容的图片,在黑盒测试中会得到完全不同的结果?

​

0x02 同样是敏感内容,为什么结果不一样?

这里我没有继续无限扩大测试样本:因为意义不大了,仅仅通过几次上传结果,确实无法直接判断平台内部究竟使用了什么模型,更不能仅凭一次成功上传就断言某个类别“完全没有审核”。

但是从黑盒测试的角度,可以提出一个比较合理的问题:平台到底是在判断整张图片,还是在分别判断图片中的不同风险特征?

例如一张图片可能同时涉及:

图片本身的视觉内容;
图片中的文字;
人物、场景等视觉特征;
图片所属的风险类别;
用户账号本身的风险;
历史行为;
当前业务场景。

实际的内容审核系统通常也不会只有一个简单的“违规 / 正常”分类器。

例如公开的内容治理资料中,就可以看到文字风险识别、图片风险识别以及综合风险识别等不同环节,之后还会结合人工审核进行处理。

所以从黑盒角度,我更倾向于把它理解成类似下面这种结构:

当然,这只是根据测试现象建立的模型。并不能证明APP内部真的就是这么实现的。这也是黑盒测试比较有意思的地方,你只能看到输入和输出。真正的模型、阈值、模型之间怎么组合,以及什么情况下进入人工审核,都藏在后面。所以我后来没有继续猜模型,而是等到了审核的解答

0x03 审核人员的答复

这波是审核先来联系,给我讲了漏洞的定级问题,并且解答了疑惑

实际审核并不是简单的:用户上传->AI判断->通过 / 拒绝

而是会根据内容风险、模型判断以及其他条件进入不同的审核流程,简单理解,可以拆成:

                    用户上传内容
                         │
                         ▼
                     机器审核(初步审核)
                         │
              ┌──────────┴──────────┐
              │                     │
            低风险                 高风险
              │                     │
              ▼                     ▼
             放行                 人工复核
                                    │
                              ┌─────┴─────┐
                              ▼           ▼
                             删除     必要时上报有关部门

这也解释了一个容易被忽略的问题:"上传成功"不一定等于“审核通过”。

对于用户来说,看到的是:“上传成功了,因为头像已经能让人看到了”。

但对平台来说,这个内容可能只是暂时没有在当前环节被直接拦截,后面仍然可能存在下列问题:

异步检测;
用户举报;
二次审核;
人工复核;
风险标签;
后续处置。

所以从这里开始,我觉得这次测试真正值得研究的东西已经不是:“我能不能传上一张敏感图片?”,而是:“这张图片进入平台之后,到底什么时候会被发现?”,这两个问题其实完全不一样。

0x04 风控模型到底在干什么?

如果把平台的风控逻辑抽象一下,它通常不会简单地返回一个 True / False,更常见的思路是给不同风险类别一个分数。

例如:

result = model(image)

porn_score = result["porn"]
political_score = result["political"]
violence_score = result["violence"]

max_score = max(
    porn_score,
    political_score,
    violence_score
)

if max_score >= 0.95:
    action = "BLOCK"
elif max_score >= 0.70:
    action = "REVIEW"
else:
    action = "PASS"

这里的代码当然不是 [APP] 的真实代码,只是为了说明这种风险评分机制。真正的系统可复杂得多。

比如还可能把:

risk = calculate_risk(
    image_score=image_score,
    text_score=text_score,
    account_risk=account_risk,
    behavior_risk=behavior_risk
)

if risk >= BLOCK_THRESHOLD:
    block()

elif risk >= REVIEW_THRESHOLD:
    send_to_human()

else:
    publish()

不同模型的结果最后再汇总成一个综合风险。这样一来,就很容易解释为什么:“都是敏感内容”,却不一定得到完全相同的审核结果。

因为它们可能根本就没有走完全相同的风险判断路径。

0x05 真正麻烦的是阈值

假设某个模型对一张图片给出的风险分数是:0.82。如果直接拦截阈值是:0.80,那么它就会被拦截。

如果阀值是:0.90,那么它可能进入人工审核或暂时放行。这就成了一个棘手的问题:阀值太高,会增加误杀。阀值太高,又会增加漏放。

所以内容风控一直存在一个很现实的取舍:

阈值降低
   ↓
更多内容被拦截
   ↓
误杀增加

阈值提高
   ↓
更多内容进入后续流程
   ↓
漏放风险增加

因此从测试人员的角度看,看到一次“成功上传”其实并不能直接推导出:“模型完全识别不了这种内容。”,它更可能说明:当前样本、当前模型、当前阈值和当前审核链路,没有在这个时间点直接阻断它。这也是我为什么后来关注时间。

0x06 人工审核是怎么接进来的?

机器审核解决的是规模问题,人工审核解决的是机器比较难直接判断的问题。

可以简单抽象成:

def moderation(content, user):

    scores = model.predict(content)

    risk = calculate_risk(
        scores,
        user.account_risk,
        user.history
    )

    if risk >= HIGH_RISK:
        return BLOCK

    if risk >= REVIEW:
        return HUMAN_REVIEW

    return PASS

进入人工审核后,又是另外一套流程:

def human_review(task):

    result = reviewer.judge(task)

    if result == "VIOLATION":
        block(task)

    elif result == "NORMAL":
        publish(task)

    else:
        escalate(task)

这里真正的问题是:人不是无限的。

模型可以同时处理大量图片、视频和文本,但人工审核必须一个任务一个任务地处理,这就产生了一个很现实的东西:审核队列。

0x07 当内容一直产生的时候

假设某个时间段产生:1000 条待审核内容 / 分钟,人工审核团队只能处理:600 条 / 分钟,那么第一分钟就会留下:400 条。第二分钟如果继续产生 1000 条:400 + 1000 - 600 = 800。第三分钟:800 + 1000 - 600 = 1200,队列会越来越长。

当然,真实平台不会简单到只有一个固定数字。可能还有自动处置、不同优先级、风险分层、更多审核人员以及其他机制。

但基本矛盾是一样的:只要内容产生速度和人工处理速度之间存在差距,就会出现时间窗口。

而这个时间窗口,对于内容风控来说非常重要,因为内容不是静态放在那里等审核的。它可能已经被其他用户看到了。

0x08 直播场景把这个问题放大了

这里讲一下近期某短手遭遇到的业务层饱和式内容洪泛攻击:黑灰产滥用平台正常开播接口,批量操控上万僵尸账号同步开播推送违规直播流。

头像和直播最大的区别,就是:头像是一张图片,直播是连续产生的内容。

直播里面可能同时存在:

视频
音频
字幕
标题
弹幕
  │
  ▼
实时检测
  │
  ▼
风险判断
  │
  ├── 正常
  │
  └── 风险
        │
     ┌──┴──┐
     ▼     ▼
  自动处置  人工复核

公开资料也能看到,平台的内容治理并不是只靠一种模型,而是将机器审核、风险识别以及人工审核等环节组合起来;例如某手公开披露的内容审核机制覆盖直播等场景,并采用机器审核与人工审核结合的方式,这时候机器审核和人工审核的特点就非常明显了。

​

机器适合:

大规模处理;
快速筛选;
对明确特征进行判断。

人工更适合:

判断上下文;
处理模型不确定的内容;
对复杂情况进行最终判断。


但人工也有一个天然限制:处理速度有限。

所以直播场景里面,真正需要关注的就不仅仅是:“模型有没有识别出来?”

还包括:“模型什么时候识别出来?”、“什么时候进入人工队列?”、“人工什么时候看到?”、“最终处置什么时候发生?”,而且攻击者也会两层对抗,一层是:批量养号、控制僵尸账号,拿到合法开播权限;另一层:部分流使用插帧 / GOP 编码欺骗,I 帧合规、P 帧藏违规内容,进一步降低模型检出率,增加送入人工队列的样本量,放大人工压力。

最终导致事故的发生,瓶颈不只是 “人工看不过来”,还有审核系统与业务处置系统之间的链路时延、并发扩容能力:就算模型识别出违规,封禁 / 断流接口在海量请求下也可能拥堵,出现 “检测到风险,但处置执行不下去” 的情况。

0x09 两个测试放在一起以后

把头像测试和直播场景放在一起以后,我觉得这件事情就比较清楚了。

头像测试告诉我:不同类型的敏感内容,在同一个业务场景里,也可能出现不同的审核结果。

直播场景则进一步告诉我:即使内容进入了审核链路,发现、复核和最终处置也不一定发生在同一个时间点。

所以这并不是简单的:“AI 审核不行。”

实际上机器审核解决的是规模问题,人工审核解决的是机器难以判断的问题。真正容易产生问题的地方,是两者之间的衔接。

​

也就是:

内容产生
   ↓
机器看到
   ↓
风险判断
   ↓
进入审核
   ↓
人工看到
   ↓
最终处置

这几个时间点之间,并不是天然重合的。

所以我后来反而觉得,比起问:“为什么这张图片能上传?”,更值得问的是:“它上传之后,多久才会被发现?”

0x0A 测试边界

这次测试主要是黑盒观察,并没有对平台审核系统进行大规模压力测试。头像部分主要观察了不同类型内容在同一个上传入口下的处理结果,直播部分则主要观察实际审核流程以及人工审核反馈。

我没有通过批量生成违规内容、持续制造大量审核任务等方式去主动压测平台,也没有尝试扩大到大规模样本。

因此,这次测试能够确认的东西主要有:

不同敏感内容在头像场景下存在不同的审核结果;
平台实际存在机器审核与人工审核等后续流程;
连续产生的内容会涉及人工审核的处理能力;
内容发现、审核和最终处置并不一定是同一个时间点。

但仅凭这次测试,无法确认:

APP使用的具体模型;
模型的具体阈值;
各类别的实际漏检率;
所有同类图片是否都可以通过;
平台整体审核队列规模;
某一次直播事件是否代表平台整体情况。

所以我更愿意把这次测试定义成:一次针对内容风控模型、人工审核和审核延迟的黑盒观察。而不是简单地说:“我发现了一个可以稳定绕过审核的漏洞。”

0x0B 最后结束

这里说一下,我因为这个漏洞没有得到赏金,而是吃了一份运营人员的警告,因为我上传的一些图片在后台都能显示,但是这件事情其实比“模型能不能识别”复杂得多。

所以我大概理解,内容风控真正存在的链路是

内容产生
    ↓
模型识别
    ↓
风险判断
    ↓
人工复核
    ↓
最终处置

而每一步之间,都可能存在时间差。所以这次测试给我留下最深的一个问题,并不是“这张图为什么过了?”,而是:“它过了以后,平台多久才能发现?”,对于内容风控来说,这两个问题其实完全不是一回事。