125 lines
3.8 KiB
Markdown
125 lines
3.8 KiB
Markdown
# 富文本过滤
|
|
|
|
:::tip 说明
|
|
富文本编辑器内容存在存储型 XSS 风险,通过 `bleach` 库实现 HTML 白名单清洗,在落盘前统一过滤危险内容。源码位于 `src/utils/rich_text.py`,涉及富文本的模块(文章、通知公告等)均调用 `save_content()` 处理。
|
|
:::
|
|
|
|
## 安全威胁
|
|
|
|
富文本编辑器允许用户输入 HTML 内容,攻击者可能注入:
|
|
|
|
```
|
|
1. <script> 标签 —— 执行任意 JavaScript
|
|
2. on* 事件属性 —— 如 onerror="alert(1)"
|
|
3. javascript: 协议 —— 如 <a href="javascript:alert(1)">
|
|
4. CSS 表达式 —— 如 style="background:expression(alert(1))"
|
|
```
|
|
|
|
## 白名单配置
|
|
|
|
位于 `src/utils/rich_text.py`,定义了严格的白名单:
|
|
|
|
### 允许的标签
|
|
|
|
```python
|
|
_RICH_TEXT_TAGS = {
|
|
'p', 'br', 'hr', 'strong', 'em', 'b', 'i', 'u', 's', 'small', 'sub', 'sup',
|
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
'ul', 'ol', 'li', 'blockquote', 'pre', 'code',
|
|
'span', 'div', 'a', 'img',
|
|
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption',
|
|
}
|
|
```
|
|
|
|
### 允许的属性
|
|
|
|
```python
|
|
_RICH_TEXT_ATTRS = {
|
|
'*': ['class', 'style'],
|
|
'a': ['href', 'title', 'target', 'rel', 'name'],
|
|
'img': ['src', 'alt', 'title', 'width', 'height'],
|
|
'td': ['colspan', 'rowspan', 'width', 'height', 'align'],
|
|
'th': ['colspan', 'rowspan', 'width', 'height', 'align'],
|
|
'table': ['width', 'border', 'cellspacing', 'cellpadding', 'align'],
|
|
}
|
|
```
|
|
|
|
### 允许的协议
|
|
|
|
```python
|
|
_RICH_TEXT_PROTOCOLS = {'http', 'https', 'mailto', 'tel'}
|
|
```
|
|
|
|
## 清洗流程
|
|
|
|
`save_content()` 函数处理富文本内容:
|
|
|
|
```python
|
|
def save_content(content, title, directory):
|
|
# 1. 白名单清洗:剥离 script/事件属性/javascript: 协议等 XSS 向量
|
|
content = _RICH_TEXT_CLEANER.clean(content)
|
|
|
|
# 2. 兜底:剥离 style 中遗留的 CSS 表达式等危险向量
|
|
content = _strip_dangerous_style(content)
|
|
|
|
# 3. 提取并迁移内容中的本地图片
|
|
image_urls = re.findall('img src="(.*?)"', content, re.S)
|
|
for url in image_urls:
|
|
image = save_file(url, directory)
|
|
if image:
|
|
content = content.replace(url, "[IMG_URL]" + image)
|
|
|
|
# 4. 设置图片 alt 属性为文章标题
|
|
title = html.escape(str(title) if title else "", quote=True)
|
|
if "alt=\"\"" in content and title:
|
|
content = content.replace("alt=\"\"", "alt=\"" + title + "\"")
|
|
|
|
return content
|
|
```
|
|
|
|
## CSS 表达式防护
|
|
|
|
`bleach` 仅按属性白名单过滤,对 `style` 值中的 CSS 表达式不做检查。额外增加正则兜底:
|
|
|
|
```python
|
|
_DANGEROUS_CSS_RE = re.compile(r'expression\s*\(|javascript\s*:', re.IGNORECASE)
|
|
|
|
def _strip_dangerous_style(content):
|
|
"""丢弃含危险 CSS 表达式的 style 属性"""
|
|
def _drop(match):
|
|
return '' if _DANGEROUS_CSS_RE.search(match.group(0)) else match.group(0)
|
|
return _STYLE_ATTR_RE.sub(_drop, content)
|
|
```
|
|
|
|
## 文件 URL 处理
|
|
|
|
`get_file_url()` 函数拼接文件域名生成完整访问 URL:
|
|
|
|
```python
|
|
def get_file_url(path):
|
|
if not path:
|
|
return ""
|
|
if path.find(FASTAPI_FILE_URL) != -1:
|
|
return path
|
|
return FASTAPI_FILE_URL + path
|
|
```
|
|
|
|
## 临时文件迁移
|
|
|
|
`save_file()` 函数将临时目录中的文件迁移到正式存储目录:
|
|
|
|
```
|
|
临时路径: {temp}/{日期}/{文件名}
|
|
正式路径: {upload_dir}/{分类}/{日期}/{文件名}
|
|
```
|
|
|
|
迁移时会做路径穿越检查,确保不会跳出 `UPLOAD_DIR` 目录。
|
|
|
|
:::tip 温馨提示
|
|
富文本内容在 service 层调用 `save_content()` 处理,而不是在 endpoint 层。所有涉及富文本的模块(文章、通知公告等)都应使用此函数清洗内容。
|
|
:::
|
|
|
|
## 总结
|
|
|
|
富文本过滤通过 `bleach` 白名单清洗 + CSS 表达式正则兜底两层防护,有效防止存储型 XSS 攻击。同时自动处理内容中的图片文件迁移和 URL 拼接,确保文件路径安全且可访问。
|