第 9 章:安全、权限与内容治理

一、安全是多层防线,不是一个 before_action

Mastodon 同时面对三类主体:本地用户、第三方 OAuth client、远端 ActivityPub server。安全检查必须分布在不同边界:

HTTP middleware / OAuth
  → Controller authentication + scope
    → Policy / current account authorization
      → Service invariant
        → Serializer field visibility
          → ActivityPub signature / audience
            → Worker re-check at execution time

每层解决的问题不同,不能用其中一层替代全部。

二、OAuth 与 access token

第三方客户端使用 OAuth application 和 access token 调用 REST/Streaming。token 不只是“登录成功证明”,还包含 scope 和 resource owner。读 API 时要检查:

  1. endpoint 要求哪些 scope;
  2. token 是否过期/撤销;
  3. current user 与 current account 如何映射;
  4. streaming 是否使用同一套 scope 语义;
  5. serializer 是否根据应用/用户隐藏字段。

发帖、修改账户、读取私密通知、管理操作都不应仅凭“用户已经登录”判断。

三、Policy 与 service invariant 的区别

Policy

回答“这个主体对这个对象能否做某动作”。例如当前账户能否删除 status、读取 private status、接受 follow request。

Service invariant

回答“无论调用方是谁,这个用例本身必须满足什么条件”。例如媒体必须属于账户且 ready、引用私密状态必须满足 mention 条件、状态编辑不能跨越允许窗口。

Controller policy:挡住普通用户的非法入口
Service invariant:防止内部调用/异步重放绕过规则
Database constraint:防止并发和数据层破坏不变量

四、可见性矩阵

visibility作者followers被提及者公共流远端联邦
public通常是按 audience
unlisted详情/用户流,避免公共推荐按 audience
private相关关系followers/inbox
direct指定收件人指定收件人指定 inbox
limited受限按关系按规则按协议语义

具体实现会叠加 block、mute、silence、suspend、domain block、quote policy、list visibility 和保护账户状态。表格是心智模型,源码中的 policy/filter 才是最终事实。

五、ActivityPub 安全

远端请求要经过多项防护:

  • HTTP Signature / Linked Data Signature 验证;
  • actor 与 key 的关联;
  • content type 和 JSON-LD context;
  • canonical URI 和来源域名;
  • 远端 dereference 的 SSRF/内网防护;
  • authorized actor 与对象 owner 检查;
  • 防止同一 activity 被重复应用;
  • 防止恶意超大 payload、深层 JSON 或无界媒体抓取。

签名验证证明“请求由持有私钥的一方发出”,不等于“活动内容一定有权限在本地执行”。授权和内容规则仍需独立检查。

六、反垃圾与内容安全

Antispam、敏感标记、内容警告、关键词过滤、域名阻断、silence/suspend 是不同机制:

  • 反垃圾决定是否允许写入或静默丢弃;
  • 内容警告决定 UI 展示方式;
  • phrase filter 决定 viewer 是否隐藏;
  • domain block 决定实例级联邦与可见性;
  • silence/suspend 决定账户传播与登录/互动能力。

“用户看不到”可能是作者权限、viewer filter、实例 moderation 或联邦失败,排障时要区分。

七、速率限制、锁和幂等

高风险/高成本操作需要不同的保护:

  • 登录、搜索、API 写入的 rate limit;
  • Lockable 防止同一账户并发发帖/编辑冲突;
  • Redis mutex 防止重复重建或重复 delivery;
  • idempotency key 防止客户端重试创建重复状态;
  • Sidekiq unique/业务唯一约束防止通知重复。

分布式锁必须有过期时间和失败处理;锁不是数据一致性的替代品,真正的不变量还需要事务和约束。

八、Worker 为什么必须重新检查权限

用户点击删除后,job 可能几秒后执行;期间账户可能被 suspend、status 可能被修改、远端对象可能已经删除。worker 不应假设 enqueue 时的状态永久有效。

def perform(status_id)
  status = Status.find_by(id: status_id)
  return if status.nil? || status.destroyed?
 
  # 基于当前状态决定是否继续
end

实际实现会更复杂,但原则一致:payload 传 ID,执行时重新读取并判断。

九、管理后台是高权限域

管理 controller、policy、navigation visibility、审计日志和后台 worker 应一起读。隐藏 Sidekiq/管理菜单只是 UI 优化,不是授权;真正权限要落在 policy、controller authorization 和管理 action 级别。

十、常见安全误区

误区 1:前端不显示按钮就安全

攻击者可以直接调用 REST/ActivityPub 入口。后端必须拒绝。

误区 2:远端签名通过就可信

签名只证明密钥控制,payload 仍需 schema、授权、对象和域名验证。

误区 3:数据库查询到对象就能返回

所有 viewer-dependent API 都必须经过 policy/filter/serializer。

误区 4:异步 job 不需要再检查

队列延迟改变了执行时世界,worker 必须面向当前事实。

十一、源码入口清单

十二、小结

Mastodon 的安全设计是“协议、身份、领域、投影、异步”多边界共同守护。下一章把所有结论落回部署与测试:如何启动这些进程、验证变更、观察失败并安全扩展。