framework
framework/react/security/xss-sanitization.md
high
react-xss-sanitization
React XSS 防护:默认转义安全,dangerouslySetInnerHTML 与 href 用户输入是唯一缺口。Use when 渲染用户 HTML / 富文本 / Markdown / 拼 href 跳转链接时
dangerouslysetinnerhtmldompurifysanitizexss 防护富文本消毒markdown 渲染href 注入协议白名单
paths
frontend/src/**/*.tsxfrontend/src/**/*.ts
React · XSS 防护
规则
决策点:内容怎么进 DOM 决定要不要消毒。
| 写法 | 是否转义 | 处置 |
|---|---|---|
{userInput} 文本插值 |
React 默认转义,安全 | 直接用 |
dangerouslySetInnerHTML |
绕过转义 | 插入前必经 DOMPurify.sanitize |
| 富文本 / Markdown 渲染 | 输出 HTML | 白名单消毒后再 dangerouslySetInnerHTML |
href={userInput} |
可被 javascript: 注入 |
校验协议白名单,非白名单降级 # |
消毒与协议校验下沉到纯函数,组件体只编排。
// frontend/src/utils/sanitize.ts
import DOMPurify from "dompurify";
// 富文本白名单消毒:仅放行排版标签与安全属性
export const sanitizeRichHtml = (raw: string): string =>
DOMPurify.sanitize(raw, {
ALLOWED_TAGS: ["p", "br", "strong", "em", "ul", "ol", "li", "a", "h1", "h2", "code"],
ALLOWED_ATTR: ["href", "title"],
});
// 链接协议白名单:只允许 http/https/mailto,否则降级为锚点
const SAFE_PROTOCOLS = ["http:", "https:", "mailto:"];
export const safeHref = (raw: string | null | undefined): string => {
if (!raw) return "#";
try {
const url = new URL(raw, window.location.origin);
return SAFE_PROTOCOLS.includes(url.protocol) ? raw : "#";
} catch {
return "#";
}
};
反例·正例
// 反例:用户 HTML 直接塞进 DOM,<script>/onerror 可执行
const ArticleBad = ({ userHtml }: { userHtml: string }) => (
<div dangerouslySetInnerHTML={{ __html: userHtml }} />
);
// 反例:href 直接放用户输入,javascript:alert(1) 可注入
const LinkBad = ({ rawUrl }: { rawUrl: string }) => <a href={rawUrl}>详情</a>;
import { sanitizeRichHtml, safeHref } from "@/utils/sanitize";
// 正例:富文本先消毒再渲染
const Article = ({ userHtml }: { userHtml: string }) => {
// 步骤 1:白名单消毒,剥离脚本与危险属性
const clean = sanitizeRichHtml(userHtml);
// 步骤 2:消毒后内容才允许进入 DOM
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
};
// 正例:href 先过协议白名单
const Link = ({ rawUrl }: { rawUrl: string }) => {
// 步骤 1:校验协议,非白名单降级为 #
const href = safeHref(rawUrl);
// 步骤 2:渲染受信链接
return <a href={href} rel="noopener noreferrer">详情</a>;
};
自检
- [ ] 纯文本走
{}插值,没有为省事改用dangerouslySetInnerHTML? - [ ] 每个
dangerouslySetInnerHTML的__html都来自DOMPurify.sanitize输出? - [ ] 富文本 / Markdown 消毒配了
ALLOWED_TAGS/ALLOWED_ATTR白名单? - [ ] 用户可控的
href/src过了协议白名单,非白名单降级#? - [ ] 外链补
rel="noopener noreferrer"?