必须移除<script>、<iframe>、<object>、<embed>、<applet>等可执行脚本或加载外部资源的标签;同时需剥离所有以on开头的事件属性及javascript:协议的href/src值,仅保留白名单标签并清除危险属性。

哪些HTML标签必须被移除?
富文本中真正危险的是能执行脚本或触发网络请求的标签,不是所有自定义标签都该删。重点盯住 <script>、<iframe>、<object>、<embed>、<applet> 这几类——它们可能加载外部资源、执行 JS 或绕过 CSP。像 <div>、<span> 本身不危险,但若保留 onerror、onclick 这类事件属性,照样能 XSS。
实际清洗时不能只看标签名,还要检查属性:javascript: 协议的 href、src 值,以及所有以 on 开头的属性(如 onload、onmouseover)都得一并剥离。
用 BeautifulSoup 的 decompose() 和 extract() 区别在哪?
decompose() 彻底删除节点及其所有子节点,DOM 中不留痕迹;extract() 只把节点从当前树中抽离,还能继续操作它(比如存日志或做审计)。对安全清洗来说,decompose() 更稳妥——避免误留引用导致后续意外渲染。
常见错误是用 clear() 或直接赋值 None,这只会清空内容,标签壳还在,属性也可能残留。
立即学习“Python免费学习笔记(深入)”;
推荐做法:
- 先用
soup.find_all(['script', 'iframe', 'object', 'embed', 'applet'])批量定位 - 对每个结果调用
.decompose() - 再统一清理事件属性:
[tag.attrs.pop(attr, None) for attr in list(tag.attrs) if attr.startswith('on')]
保留格式但过滤属性:为什么不能只用 strip=True?
strip=True 是针对字符串前后空白的,跟 HTML 标签安全完全无关。有人误以为它能“净化”,结果白忙活。真正要保留语义又控风险,得靠白名单策略。
例如允许 <p>、<strong>、<ul>、<li> 等排版标签,但强制清除它们的 style 属性(防 CSS 注入)和所有事件属性。可配合 BeautifulSoup 的 attrs 参数做精细控制:
soup = BeautifulSoup(html, 'html.parser')
for tag in soup.find_all(True): # 找所有标签
if tag.name not in ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a']:
tag.decompose()
else:
for attr in ['style', 'onclick', 'onload', 'onerror'] + [a for a in tag.attrs if a.startswith('on')]:
tag.attrs.pop(attr, None)
if tag.name == 'a' and tag.get('href'):
href = tag['href'].strip()
if not href.startswith(('http://', 'https://', '/')):
tag['href'] = '#' # 非法协议一律降级
为什么不能跳过 html.parser 直接用 lxml?
lxml 解析快,但默认会自动修复破损 HTML(比如补全缺失的闭合标签),这个过程可能意外引入新结构或重排 DOM,导致清洗逻辑失效。更麻烦的是,它对某些畸形嵌套(如 <script><div></script>)的处理不如 html.parser 可预测。
安全清洗第一要义是行为可复现、结果可验证。html.parser 是 Python 标准库,无额外依赖,解析行为稳定,适合做可信清洗入口。等清洗完成、DOM 干净后,再用 lxml 做后续 XPath 提取也不迟。
另外注意:不要用 html5lib,它太“宽容”,会把明显恶意片段也当成合法 HTML 重建,反而埋雷。



















