一、为什么要从源头阻断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,有几个地方得格外留心:

  1. 规则要精细:别一棍子打死所有 dangerouslySetInnerHTML,比如有些第三方组件库内部用了这个,你要是全局满扫,会报一堆与业务无关的错误。建议在规则文件里加上 paths: 排除 node_modules/ 和第三方组件目录。

  2. 消毒函数要统一:团队最好约法三章,规定一个统一的消毒函数名字,比如 sanitizeHTML,然后在规则里专门放行这种写法。

  3. 区分上下文:同一个用户输入,在HTML里、在属性里、在JavaScript里,危险程度不一样。比如用户在 <style> 标签里也可能被利用,但我们的规则没覆盖到。要逐步补充。

  4. 不能完全依赖工具:Semgrep只是锦上添花,最根本的还是要培养开发者的安全意识。机器查不到的漏洞(比如业务逻辑导致的XSS)还得靠人工review。

  5. 规则版本管理:把规则文件放进git里,和代码一起维护。每次更新规则,也要走CR流程,防止误伤。

八、总结

从源头阻断XSS,不是一句口号,而是每个写React页面的人都能做到的事情。Semgrep给了我们一把螺丝刀,我们只需要按照这篇指南里的套路,拧紧几个关键点——dangerouslySetInnerHTML、动态href、模板字符串拼接——就能把大部分XSS漏洞挡在门外。当然,工具只是辅助,真正的安全防线还在咱们写代码的指尖上。

那些花里胡哨的攻击手法,说到底都是利用了程序员的疏忽。与其每次出了漏洞再修修补补,不如一开始就把规矩立好,让机器帮我们把关。从今天开始,给你的React项目加上一套Semgrep规则吧。