一、为什么要从源头阻断XSS
咱们写React组件的时候,最怕啥?就是用户输入点啥,咱没处理好,直接往页面上怼,结果人家往输入框里塞了一段 <script>alert('你中招了')</script>,一刷新,弹个窗出来,轻则吓一跳,重则用户的cookie、token全被偷走。这就是XSS(跨站脚本攻击)的典型场景。
以前大家习惯用“事后打扫”来防XSS——比如在渲染之前拿库把字符串里的尖括号转义掉,或者在服务端做一遍过滤。但问题来了:人的记性靠不住,React项目那么大,组件那么多,总有遗漏的地方。一旦漏掉一个 dangerouslySetInnerHTML 没做清洗,就等着上安全整改单吧。所以更聪明的做法是“从源头阻断”——在代码写出来的那一刻,就用工具把可能出问题的模式直接揪出来,不让它有溜进生产环境的机会。
Semgrep 就是这样一个工具,它能让咱自定义规则,像扫雷一样精准地找到那些危险的代码模式。今天我们就拿React组件开刀,手把手写几组实用的Semgrep规则,把常见的XSS漏洞扼杀在萌芽里。
二、Semgrep是什么?怎么帮我们抓XSS?
Semgrep 是个开源静态分析工具,和传统的代码扫描不一样的是,它支持“模式匹配”——你可以写一个简单的模式,就像你在找“长得像”的代码片段。比如你写一句 dangerouslySetInnerHTML,它就能把所有用了这个属性的地方都翻出来,不管变量名是啥、上下文多乱。
2.1 Semgrep的安装和基本用法
装Semgrep特别简单,一行命令就行,前提是你电脑上已经装了Python 3.7以上版本:
# 用pip安装Semgrep
pip install semgrep
安装好之后,随便写个测试文件试一下:
// 技术栈:JavaScript (React)
function MyComponent() {
const userContent = '<img src=x onerror=alert(1)>';
return <div dangerouslySetInnerHTML={{ __html: userContent }} />;
}
然后在终端运行:
semgrep --pattern 'dangerouslySetInnerHTML' test.jsx
它会告诉你这个文件里有一处匹配到了。这只是个最简单的演示,实际咱们要写的规则要聪明得多,比如能匹配变量名、函数调用、甚至跨行的代码。
2.2 自定义规则的基本语法
Semgrep的自定义规则写在YAML文件里,结构很清晰:
# 规则文件示范:block-xss.yaml
rules:
- id: block-dangerouslySetInnerHTML
pattern: |
dangerouslySetInnerHTML={...}
message: "使用了dangerouslySetInnerHTML,有XSS风险"
languages: [javascript, typescript]
severity: ERROR
这个规则匹配了所有 dangerouslySetInnerHTML={...} 这样的写法。但实际场景里,{...} 里面可能是个表达式,比如 {__html: someVar},我们得考虑更复杂的模式。咱们后面会一步步升级。
三、React中常见的XSS漏洞模式
写React的时候,最容易捅娄子的就那么几个地方,咱一个一个捋。
3.1 dangerouslySetInnerHTML
这是React官方提供的一个“后门”,专门让你往组件里直接塞原生HTML。文档里明说了“用这玩意儿请自行承担风险”,但在展示富文本内容的时候,很多人还是会用。比如:
// 技术栈:JavaScript (React)
function RichText({ content }) {
// 危险:content来自用户,可能带恶意脚本
return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
如果不先对 content 做消毒(比如用DOMPurify清洗),这就是个明晃晃的XSS漏洞。
3.2 动态href/自定义属性注入
另一个容易出问题的地方是URL拼接。比如用户输入一个链接,你直接塞到 <a> 的href里:
// 技术栈:JavaScript (React)
function UserLink({ url }) {
// 危险:用户可能传javascript:alert(1)这样的协议
return <a href={url}>点击我</a>;
}
用户若传 javascript:alert(1) 或者 data:text/html;base64,...,点击链接就能执行脚本。React不会自动帮你清洗href里的协议,全靠开发者自觉。
3.3 eval-like的用法
虽然React里直接用 eval 的不多,但有些同学喜欢用 new Function 或者 setTimeout 传字符串:
// 技术栈:JavaScript (React)
function handleClick(userCode) {
// 危险:把用户输入的代码当脚本执行
setTimeout(userCode, 1000);
}
这种模式一旦被利用,就等于是把门钥匙交给了小偷。
四、编写Semgrep规则来捕获这些模式
现在,咱们就针对上面说的三种漏洞,写出对应的Semgrep自定义规则,每一条都会带上完整的示例和注释。
4.1 规则一:检测dangerouslySetInnerHTML
这个规则要找到所有 dangerouslySetInnerHTML 的使用,并且提示开发者必须对内容做消毒。我们不仅要匹配属性名称,还要能把变量提取出来,方便后续分析。
规则文件:block-xss-dangerously.html.yaml
# 规则一:发现所有dangerouslySetInnerHTML的使用
rules:
- id: react-xss-dangerously-set-inner-html
patterns:
- pattern: |
<... dangerouslySetInnerHTML={...} ...>
- pattern-not: |
<... dangerouslySetInnerHTML={{ __html: $FUNC($X) }} ...>
message: >
发现使用dangerouslySetInnerHTML,存在XSS风险。
请确保内容已通过DOMPurify等库消毒,例如:
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
languages: [javascript, typescript, jsx, tsx]
severity: ERROR
这个规则做了两件事:先匹配所有 dangerouslySetInnerHTML 的出现,然后排除掉那些已经包裹了函数调用的写法(即 $FUNC($X) 认为是一个消毒函数)。当然,这个模式还不够完美,实际项目中消毒函数可能是 sanitize(content, options),或者用户自定义的其他名字,我们可以后续再细化。
4.2 规则二:检测不安全的href赋值
我们需要找到所有 <a> 标签的 href 属性里,直接使用了没有被协议白名单过滤的变量。最简单的做法是:匹配 href={...} 而且内容不是 # 或者合法协议的。
规则文件:block-xss-href.yaml
# 规则二:检测href中直接使用未过滤的变量
rules:
- id: react-xss-unsafe-href
patterns:
- pattern: |
<a href={$VAR}>...</a>
- pattern-not: |
<a href={$FUNC($VAR)}>...</a>
- pattern-not: |
<a href="/...">...</a>
- pattern-not: |
<a href="http://...">...</a>
- pattern-not: |
<a href="https://...">...</a>
- pattern-not: |
<a href="mailto:...">...</a>
message: >
检测到<a>标签的href直接使用变量{$VAR},可能导致javascript:协议注入。
建议添加协议白名单过滤,例如:const safeUrl = url.startsWith('http') ? url : '#';
languages: [javascript, typescript, jsx, tsx]
severity: WARNING
这里用了多个 pattern-not 来排除常见的合法场景。实际生产环境可能还需要考虑 tel: 等协议,你可以按需添加。
4.3 规则三:检测字符串拼接的URL
有时候开发者会把用户输入拼接到 src 或者 href 前面,比如:
// 技术栈:JavaScript (React)
<iframe src={`https://maps.google.com?q=${userInput}`} />
如果 userInput 里包含 "# 或者引号,就可能破坏HTML结构,造成XSS。我们可以匹配所有模板字符串用在属性里,并且没有经过编码的。
规则文件:block-xss-template-literal-attribute.yaml
# 规则三:检测模板字符串直接用于属性值
rules:
- id: react-xss-template-literal-in-attr
pattern-either:
- pattern: |
<... ${...} ...>
- pattern: |
<... $ATTR={`...${$INPUT}...`} ...>
message: >
在属性中使用了模板字符串拼接用户输入{$INPUT},可能导致属性注入攻击。
请使用encodeURIComponent或类似方法转义,或避免直接拼接。
languages: [javascript, typescript, jsx, tsx]
severity: WARNING
这个规则用了 pattern-either 来匹配两种模式:一是整个属性值都是模板字符串,二是模板字符串里有插值。注意这里的 $ATTR 匹配任何属性名。
五、集成到CI/CD流水线,实时阻断
写好规则只是第一步,要让规则真正起作用,得把它挂到CI/CD上,让每次提交代码都自动扫描一把。比如在GitHub Actions里加个步骤:
# .github/workflows/semgrep.yml
name: Semgrep XSS Scan
on: [push, pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: path/to/your/rules/ # 指向存放规则文件的目录
publishToken: ${{ secrets.SEMGREP_TOKEN }} # 可选,用于上传结果
这样,只要有新代码提交,Semgrep就会自动扫描,发现XSS风险就直接在CI里报错,阻断合入。团队里的人看到红色报错,自然会回去改代码。
六、应用场景与技术优缺点
6.1 应用场景
这套自定义规则最适合下面几种场景:
- 大型React项目:组件多,新人多,代码审查容易遗漏,用规则批量扫描。
- 遗留代码安全改造:以前的老代码可能到处是危险写法,跑一遍规则全标出来。
- 安全审计:在上线前做一次全面扫描,确保没有低级漏洞。
6.2 技术优缺点
优点:
- 门槛低:只要你会写一点点YAML和正则思维,几分钟就能写一条规则。
- 跑得快:Semgrep是用OCaml写的,扫描几万行代码也就几秒。
- 可玩性强:可以自定义任意模式,不局限于XSS,SQL注入、硬编码密钥啥都能查。
缺点:
- 规则不够智能:比如它不知道你调用了一个消毒函数,但那个函数其实是个假的。它只看代码结构,不分析语义。
- 容易产生误报:上面的规则里用了很多
pattern-not排除,但实际项目里总有一些奇怪的写法被误杀,需要不断调优。 - 依赖维护:规则需要根据新写法不断更新,不然就过时了。
七、注意事项
用Semgrep防XSS,有几个地方得格外留心:
规则要精细:别一棍子打死所有
dangerouslySetInnerHTML,比如有些第三方组件库内部用了这个,你要是全局满扫,会报一堆与业务无关的错误。建议在规则文件里加上paths:排除node_modules/和第三方组件目录。消毒函数要统一:团队最好约法三章,规定一个统一的消毒函数名字,比如
sanitizeHTML,然后在规则里专门放行这种写法。区分上下文:同一个用户输入,在HTML里、在属性里、在JavaScript里,危险程度不一样。比如用户在
<style>标签里也可能被利用,但我们的规则没覆盖到。要逐步补充。不能完全依赖工具:Semgrep只是锦上添花,最根本的还是要培养开发者的安全意识。机器查不到的漏洞(比如业务逻辑导致的XSS)还得靠人工review。
规则版本管理:把规则文件放进git里,和代码一起维护。每次更新规则,也要走CR流程,防止误伤。
八、总结
从源头阻断XSS,不是一句口号,而是每个写React页面的人都能做到的事情。Semgrep给了我们一把螺丝刀,我们只需要按照这篇指南里的套路,拧紧几个关键点——dangerouslySetInnerHTML、动态href、模板字符串拼接——就能把大部分XSS漏洞挡在门外。当然,工具只是辅助,真正的安全防线还在咱们写代码的指尖上。
那些花里胡哨的攻击手法,说到底都是利用了程序员的疏忽。与其每次出了漏洞再修修补补,不如一开始就把规矩立好,让机器帮我们把关。从今天开始,给你的React项目加上一套Semgrep规则吧。
Comments