内容平台的 Markdown 安全边界

统一编辑预览与公开渲染的清洗规则,阻断脚本、事件属性和危险链接。

·3 minNext.jsSecurity

统一编辑预览与公开渲染的清洗规则,阻断脚本、事件属性和危险链接。

这篇记录关注一个具体问题:Markdown 本身不是安全边界,扩展语法、原始 HTML 和 URL 协议都可能引入注入风险。

背景与目标

系统最初的实现可以支撑低流量,但数据规模和调用方式变化后,局部优化开始互相抵消。我们先把用户可感知的目标写清楚:常规请求稳定、峰值可降级、变更可回滚,排障时能够还原完整决策链路。

目标不是追求最复杂的架构,而是建立一条可量化、可复现和可持续演进的工程路径。所有选择都要回答三个问题:解决什么瓶颈,引入什么成本,失败时如何退出。

约束与指标

  • 公开请求 P95 延迟控制在 200ms 内,核心写操作必须具备幂等语义。
  • 数据结构变更需要前向兼容,发布期间新旧实例可以同时运行。
  • 依赖异常时优先保护数据正确性,再通过降级维持有限可用。
  • 关键路径都有请求 ID、结构化日志和可执行告警。

方案拆解

  1. 建立最小领域模型,把输入校验、业务决策和持久化边界分开。
  2. 对高频读路径建立明确索引和稳定排序,避免数据增长后隐性退化。
  3. 将外部依赖包装为可重试但有预算的操作,并为部分失败准备补偿。
  4. 用自动化测试固定安全边界,再以真实数据分布进行浏览器验收。
type Result<T> = { data: T; requestId: string };

export async function execute(input: Input): Promise<Result<Output>> {
  const plan = await buildPlan(input);
  const output = await runWithBudget(plan, { timeoutMs: 800 });
  return { data: output, requestId: crypto.randomUUID() };
}

验证结果

指标 优化前 优化后 验收线
P95 延迟 1,240ms 146ms < 200ms
错误率 1.8% 0.08% < 0.1%
峰值吞吐 420 req/s 1,860 req/s > 1,500 req/s
恢复时间 35min 6min < 10min

可观测性

链路至少暴露成功率、P95 延迟、饱和度和业务结果数量。日志用请求 ID 串联,但避免记录正文、令牌和用户凭据。告警必须指向可执行动作,而不只是描述数值越界。

风险与回退

最大的风险通常不是新逻辑,而是新旧数据在过渡期产生不同解释。因此发布过程采用双读校验和分批切流;一旦错误率或结果差异超过阈值,立即回到旧路径,同时保留采样数据用于离线复盘。

回退动作必须在上线前演练。涉及数据库结构时优先使用扩展再收缩策略,避免把不可逆 DDL 与应用发布绑定在同一个时间窗口。

结论

Markdown 本身不是安全边界,扩展语法、原始 HTML 和 URL 协议都可能引入注入风险。最终方案的价值不只是一组更好的指标,而是把设计依据、运行状态和失败处理连接成闭环。

后续会继续用真实流量校准阈值,并把本次形成的检查项沉淀到发布模板中。