RSS作为内容聚合的轻量协议,被大量站点用来拉取第三方新闻、博客更新。但很多团队在接入外部feed时,只关心内容展示,忽略了源本身可能携带的恶意负载。一旦解析环节缺乏约束,攻击者就能通过特制文档窃取服务端文件或植入脚本。

一、RSS接入面临的主要安全风险
最常见的两类问题是XML外部实体注入(XXE)与内容注入。XXE利用XML解析器对外部实体的支持,在feed中声明本地文件路径,解析时把/etc/passwd之类内容回显到item里。内容注入则是通过<description>中嵌入<script>或伪造的<img onerror>,在后端渲染或前端插入DOM时执行。
另一个容易被忽视的点是URL注入。部分聚合程序会用feed中的<link>直接拼接跳转或爬虫任务,若未校验协议头,就可能造成SSRF,让内网服务被间接访问。理解这些路径,才能针对性设防。
1.1 XXE原理简析
当解析器启用外部实体且未限制网络与文件系统访问时,如下片段会在解析阶段触发文件读取:
<?xml version="1.0"?>
<!DOCTYPE rss [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<rss>
<channel>
<item>
<title>test</title>
<description>&xxe;</description>
</item>
<channel>
</rss>
上述代码中的&xxe;在解析后会被替换为文件内容。如果后端将description原样存储并展示,敏感信息就外泄了。因此关闭外部实体是第一道防线。
1.2 内容注入场景
即便没有XXE,feed中的HTML片段也可能在前端造成问题。例如某条目描述里写有<img src=x onerror=alert(1)>,若用innerHTML直接插入页面,脚本便会运行。后端若仅做数据库转义而前端信任了富文本,依然有洞。
二、服务端安全解析实践
推荐在服务器端使用禁用外部实体的解析库,并对输出字段做白名单过滤。以Node.js为例,可用fast-xml-parser并设置ignoreAttributes为false但禁用实体,再配合sanitize-html清理描述字段。
const fs = require('fs');
const parser = require('fast-xml-parser');
const sanitizeHtml = require('sanitize-html');
// 读取外部rss文本(示例)
const xml = fs.readFileSync('feed.xml', 'utf8');
// 解析时关闭外部实体相关特性
const result = parser.parse(xml, {
ignoreAttributes: false,
attributeNamePrefix: '@_',
// 该库默认不解析DOCTYPE实体,天然缓解XXE
});
const items = result.rss.channel.item;
items.forEach(it => {
// 仅允许安全标签,阻断script与事件属性
const cleanDesc = sanitizeHtml(it.description, {
allowedTags: ['p', 'a', 'br', 'strong', 'em'],
allowedAttributes: { a: ['href'] }
});
console.log('标题:', it.title, '描述:', cleanDesc);
});
上面代码展示了最小可用流程。fast-xml-parser本身不处理DOCTYPE实体,从根源避免XXE;sanitize-html则确保描述里只剩基础排版标签。若项目使用Python,可用defusedxml替换标准库xml以禁用实体。
需要注意的是,白名单要比黑名单可靠。不要试图用正则删掉onerror,因为攻击者可用大小写、换行绕过。明确允许哪些标签和属性,其余一律丢弃,才符合安全设计原则。
2.1 链接字段校验
对<link>和<guid>中的地址,应检查协议是否为http或https,并屏蔽指向内网的IP段。下面给出一个简单的校验函数:
import re
def is_safe_url(url):
# 仅允许http/https
if not re.match(r'^https?://', url):
return False
# 禁止内网地址示例
if re.search(r'192.168.|10.|127.0.0.1', url):
return False
return True
feed_link = 'https://ipipp.com/rss'
print(is_safe_url(feed_link))
该函数虽简单,但能挡住大部分SSRF前置条件。生产环境还可结合DNS重绑定防护,在请求前再次解析确认IP非内网。
三、前端渲染的注意事项
即便后端已清洗,前端也应避免直接用innerHTML渲染第三方内容。使用textContent或经过框架转义的插值(如React的{})可彻底断开脚本执行链。如果必须展示富文本,可复用服务端的清洗规则。
<!-- 错误做法:直接插入 -->
<div id="desc"></div>
<script>
document.getElementById('desc').innerHTML = rssItem.description;
</script>
<!-- 正确做法:文本赋值 -->
<div id="safe"></div>
<script>
document.getElementById('safe').textContent = rssItem.title;
</script>
上例对比了危险与安全的DOM操作。textContent会把所有内容当作纯文本,攻击者构造的标签不再被解析。现代框架默认转义,但使用v-html或dangerouslySetInnerHTML时仍需谨慎。
3.1 签名与源可信
对于重要feed,可要求提供方在HTTP头或单独文件中给出哈希签名,服务端拉取后校验。这样即使中间被篡改,也能及时发现。该机制类似SRI,但应用于RSS层面。
| 措施 | 防御对象 | 实施层 |
|---|---|---|
| 禁用外部实体 | XXE | 解析库 |
| 白名单清洗 | HTML注入 | 服务/前端 |
| URL校验 | SSRF | 服务端 |
| 内容签名 | 篡改 | 传输层 |
表中归纳了核心手段。实际项目中应组合使用,而非依赖单一方案。RSS虽简单,但接入不可信源时,安全模型和对待用户上传文件一致。
四、总结与检查清单
部署RSS聚合前,建议对照以下清单自查:解析库是否禁用外部实体;描述字段是否经白名单过滤;链接是否校验协议与IP;前端是否避免危险插入;重要源是否启用签名。把这些动作变成接入流程的一部分,就能在享受聚合便利的同时,把注入与漏洞风险压到最低。
安全不是一次性改造,而是持续约束。当新增feed源时,重复同样的校验逻辑,系统便不会因某个第三方文档而失守。