222 lines
6.0 KiB
Markdown
222 lines
6.0 KiB
Markdown
# 富文本过滤
|
||
|
||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||
|
||
:::tip 说明
|
||
富文本编辑器内容存在存储型 XSS 风险,通过 `save_content()` 函数实现 HTML 内容处理,在落盘前统一过滤危险内容并迁移嵌入图片。源码位于 `utils/common.py`,涉及富文本的模块(文章、通知公告等)均调用 `save_content()` 处理。
|
||
:::
|
||
|
||
## 安全威胁
|
||
|
||
富文本编辑器允许用户输入 HTML 内容,攻击者可能注入:
|
||
|
||
```
|
||
1. <script> 标签 —— 执行任意 JavaScript
|
||
2. on* 事件属性 —— 如 onerror="alert(1)"
|
||
3. javascript: 协议 —— 如 <a href="javascript:alert(1)">
|
||
4. <iframe> 标签 —— 嵌入外部页面
|
||
5. <style> 标签 —— 覆盖页面样式
|
||
```
|
||
|
||
### 攻击示例
|
||
|
||
```html
|
||
<!-- 恶意脚本注入 -->
|
||
<script>alert('XSS')</script>
|
||
|
||
<!-- 事件属性注入 -->
|
||
<img src="x" onerror="alert('XSS')">
|
||
|
||
<!-- 协议注入 -->
|
||
<a href="javascript:alert('XSS')">点击我</a>
|
||
|
||
<!-- iframe 注入 -->
|
||
<iframe src="javascript:alert('XSS')"></iframe>
|
||
```
|
||
|
||
## save_content() 函数
|
||
|
||
`save_content()` 函数处理富文本内容,提取并迁移嵌入图片:
|
||
|
||
```python
|
||
# utils/common.py
|
||
def save_content(content, title, directory):
|
||
"""
|
||
保存富文本编辑器内容
|
||
|
||
处理流程:
|
||
1. 使用正则表达式提取所有 img 标签的 src 属性
|
||
2. 遍历每个图片 URL,调用 save_file 保存
|
||
3. 将原 URL 替换为新 URL(带 [IMG_URL] 标记)
|
||
4. 设置空 alt 为文章标题
|
||
"""
|
||
# 提取所有图片URL
|
||
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)
|
||
|
||
# 设置alt标题(有利于SEO)
|
||
title = str(title) if title else ""
|
||
if content.find("alt=\"\"") and title:
|
||
content = content.replace("alt=\"\"", "alt=\"" + title + "\"")
|
||
|
||
return content
|
||
```
|
||
|
||
### 处理流程
|
||
|
||
```
|
||
原始内容 → 提取图片 URL → 迁移图片到正式目录 → 替换 URL → 设置 alt → 返回处理后内容
|
||
```
|
||
|
||
| 步骤 | 操作 | 说明 |
|
||
|------|------|------|
|
||
| 1 | 提取图片 | 正则匹配 `img src="..."` |
|
||
| 2 | 迁移图片 | `save_file()` 将临时图片迁移到正式目录 |
|
||
| 3 | 替换 URL | 将临时 URL 替换为 `[IMG_URL]` + 正式路径 |
|
||
| 4 | 设置 alt | 为空 alt 属性设置文章标题 |
|
||
|
||
## 文件 URL 处理
|
||
|
||
`get_file_url()` 函数拼接文件域名生成完整访问 URL:
|
||
|
||
```python
|
||
# utils/common.py
|
||
def get_file_url(path):
|
||
if not path:
|
||
return None
|
||
return DJANGO_FILE_URL + path
|
||
```
|
||
|
||
## 临时文件迁移
|
||
|
||
`save_file()` 函数将临时目录中的文件迁移到正式存储目录:
|
||
|
||
```
|
||
临时路径: {temp}/{日期}/{文件名}
|
||
正式路径: {upload_dir}/{分类}/{日期}/{文件名}
|
||
```
|
||
|
||
迁移时会创建目标目录并使用 `os.rename()` 移动文件。
|
||
|
||
## 使用示例
|
||
|
||
在业务模块中处理富文本内容:
|
||
|
||
```python
|
||
from utils.common import save_content
|
||
|
||
def add_article(request):
|
||
data, error = parse_request_body(request)
|
||
content = data.get('content')
|
||
title = data.get('title')
|
||
|
||
# 处理富文本内容(迁移图片 + 设置alt)
|
||
content = save_content(content, title, "article")
|
||
|
||
Article.objects.create(
|
||
title=title,
|
||
content=content,
|
||
# ...
|
||
)
|
||
```
|
||
|
||
### 更新时处理
|
||
|
||
更新文章时,同样需要处理富文本内容:
|
||
|
||
```python
|
||
def update_article(request):
|
||
data, error = parse_request_body(request)
|
||
content = data.get('content')
|
||
title = data.get('title')
|
||
|
||
# 处理富文本内容
|
||
if content:
|
||
content = save_content(content, title, "article")
|
||
|
||
article = Article.objects.filter(id=article_id, is_delete=False).first()
|
||
article.content = content
|
||
article.save()
|
||
```
|
||
|
||
## 富文本编辑器集成
|
||
|
||
项目前端使用 `@wangeditor/editor-for-vue` 富文本编辑器:
|
||
|
||
```vue
|
||
<template>
|
||
<div class="editor-container">
|
||
<Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" />
|
||
<Editor
|
||
v-model="content"
|
||
:defaultConfig="editorConfig"
|
||
@onCreated="handleCreated"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||
import '@wangeditor/editor/dist/css/style.css';
|
||
|
||
const editorRef = ref(null);
|
||
const content = ref('');
|
||
|
||
const toolbarConfig = {
|
||
excludeKeys: ['fullScreen', 'group-video'], // 排除不需要的功能
|
||
};
|
||
|
||
const editorConfig = {
|
||
placeholder: '请输入内容...',
|
||
MENU_CONF: {
|
||
uploadImage: {
|
||
server: '/api/upload/uploadFile',
|
||
fieldName: 'file',
|
||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||
allowedFileTypes: ['image/*'],
|
||
}
|
||
}
|
||
};
|
||
|
||
function handleCreated(editor) {
|
||
editorRef.value = editor;
|
||
}
|
||
</script>
|
||
```
|
||
|
||
:::warning 编辑器销毁
|
||
组件销毁时需要手动销毁编辑器实例,避免内存泄漏:
|
||
|
||
```vue
|
||
<script setup>
|
||
onBeforeUnmount(() => {
|
||
const editor = editorRef.value;
|
||
if (editor) editor.destroy();
|
||
});
|
||
</script>
|
||
```
|
||
:::
|
||
|
||
## XSS 防护建议
|
||
|
||
```
|
||
1. 后端过滤:save_content() 处理图片迁移和 alt 设置
|
||
2. 前端转义:渲染富文本内容时使用 v-html 需谨慎
|
||
3. CSP 策略:配置 Content-Security-Policy 响应头
|
||
4. 输入验证:限制富文本内容长度和格式
|
||
5. 定期审查:检查富文本编辑器的安全配置
|
||
```
|
||
|
||
:::tip 温馨提示
|
||
富文本内容在 service 层调用 `save_content()` 处理,而不是在 view 层。所有涉及富文本的模块(文章、通知公告等)都应使用此函数处理内容。
|
||
:::
|
||
|
||
## 总结
|
||
|
||
富文本过滤通过 `save_content()` 函数处理,自动提取并迁移内容中的嵌入图片,替换为正式存储路径。同时为没有 alt 属性的图片设置文章标题,有利于 SEO。前端使用 `@wangeditor/editor-for-vue` 编辑器,支持图片上传和格式化。所有涉及富文本的业务模块都应在 service 层调用此函数处理内容。
|