JS `Static Code Analysis` (静态代码分析) 工具 (`ESLint`, `SonarJS`) 自定义规则用于安全审计

各位观众老爷们,大家好!今天咱们来聊聊JS安全审计这事儿,但咱不搞那些虚头巴脑的理论,直接上干货,教你用自定义规则来武装你的代码。

开场白:代码界的“朝阳群众”

话说江湖险恶,代码世界也一样。各种XSS、SQL注入、CSRF,防不胜防。咱们程序员每天辛辛苦苦搬砖,结果一不小心就被黑客给端了老窝,你说憋屈不憋屈?

所以啊,咱们得想办法,在代码上线之前,就把这些潜在的风险给揪出来。这就是静态代码分析的意义所在,它就像代码界的“朝阳群众”,时刻监视着你的代码,一旦发现可疑之处,立刻报警!

主角登场:ESLint & SonarJS

今天的主角是两位:ESLint 和 SonarJS。

  • ESLint: JS 界的“老大哥”,语法检查、代码风格统一不在话下,更重要的是,它支持自定义规则,允许我们根据自己的安全需求,定制专属的“安全卫士”。
  • SonarJS: SonarQube 的 JS 插件,功能更强大,除了静态代码分析,还能进行代码质量评估、漏洞检测等。

自定义规则:打造你的专属“安全卫士”

自定义规则是核心,它允许我们针对特定的安全漏洞,编写检测逻辑,让工具自动扫描代码,发现潜在的风险。

案例一:防止 XSS 攻击

XSS (Cross-Site Scripting) 攻击是最常见的 Web 安全漏洞之一。攻击者通过注入恶意脚本到网页中,窃取用户数据或执行恶意操作。

咱们可以写一个 ESLint 规则,检测代码中是否存在将用户输入直接插入到 DOM 中的情况。

// custom-eslint-rules/no-innerhtml.js

module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '禁止使用 innerHTML 插入用户输入,防止 XSS 攻击',
      category: 'Possible Errors',
      recommended: 'error',
    },
    fixable: null,  // or "code" if you want to provide a fix
    schema: [], // no options
  },

  create: function (context) {
    return {
      MemberExpression: function (node) {
        if (node.property.name === 'innerHTML') {
          const objectName = node.object.name;
          // 这里可以添加更复杂的逻辑来判断 `objectName` 是否是用户输入
          // 比如,判断它是否来自于 URL 参数、表单数据等

          context.report({
            node: node,
            message: '避免使用 innerHTML 插入用户输入,可能导致 XSS 攻击',
          });
        }
      },
    };
  },
};

代码解读:

  • meta: 定义规则的元数据,包括类型、描述、分类、推荐级别等。
  • create: 核心函数,接收 context 对象,用于注册访问者,并在代码中发现问题时进行报告。
  • MemberExpression: 访问成员表达式,比如 element.innerHTML
  • node.property.name === 'innerHTML': 判断是否是访问 innerHTML 属性。
  • context.report: 报告问题,node 指定问题节点,message 指定错误信息。

使用方法:

  1. 创建 custom-eslint-rules 文件夹,将 no-innerhtml.js 放入其中。
  2. .eslintrc.js 中配置:
module.exports = {
  "plugins": ["custom-eslint-rules"],
  "rules": {
    "custom-eslint-rules/no-innerhtml": "error"
  },
  "rulesDirectory": ["custom-eslint-rules/"]
};

案例二:防止 SQL 注入

SQL 注入是另一种常见的 Web 安全漏洞。攻击者通过构造恶意的 SQL 查询,窃取或篡改数据库中的数据。

咱们可以写一个 SonarJS 规则,检测代码中是否存在拼接 SQL 语句的情况。

// SonarJS 规则示例 (需要 SonarQube 环境)

import type { RuleContext } from "sonarqube";
import { BaseNodeListener } from "./../utils/base-node-listener";

export const rule = {
  meta: {
    type: "vulnerability",
    ruleId: "no-string-based-sql-injection",
    description: "防止SQL注入风险",
    securityStandards: {
      "OWASP": ["A03:2021 - Injection"],
      "CWE": ["CWE-89"]
    }
  },
  create: (context: RuleContext) => {
    return {
      ...BaseNodeListener,
      TaggedTemplateExpression(node: any) {
        if (node.tag.name === 'sql') {
            context.report({
              node,
              message: '避免使用字符串拼接方式构建SQL语句,推荐使用参数化查询。',
            });
          }
      },
      BinaryExpression(node: any) {
        if (node.operator === '+' && (isStringConcatenation(node.left) || isStringConcatenation(node.right))) {
          context.report({
            node,
            message: '避免使用字符串拼接方式构建SQL语句,推荐使用参数化查询。',
          });
        }
      },
    };

    function isStringConcatenation(node: any): boolean {
        if(!node) return false;
      if (node.type === "Literal" && typeof node.value === "string") {
        return true;
      }
      if (node.type === "BinaryExpression" && node.operator === "+") {
        return isStringConcatenation(node.left) || isStringConcatenation(node.right);
      }
      return false;
    }
  }
};

代码解读:

  • meta: 定义规则的元数据,包括类型、描述、安全标准等。
  • TaggedTemplateExpression: 检测标记模板字符串,比如 sqlSELECT * FROM users WHERE id = ${id}“。
  • BinaryExpression: 检测二元表达式,比如 "SELECT * FROM users WHERE id = " + id
  • isStringConcatenation: 递归判断表达式是否是字符串拼接。
  • context.report: 报告问题,node 指定问题节点,message 指定错误信息。

使用方法:

  1. 将规则文件放入 SonarQube 插件目录。
  2. 在 SonarQube 中激活规则。

案例三:防止 CSRF 攻击

CSRF (Cross-Site Request Forgery) 攻击是指攻击者冒充用户发送恶意请求,执行未授权的操作。

咱们可以写一个 ESLint 规则,检测代码中是否存在未添加 CSRF token 的表单提交。

// custom-eslint-rules/require-csrf-token.js

module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '要求所有表单提交都包含 CSRF token',
      category: 'Security',
      recommended: 'error',
    },
    fixable: null,
    schema: [],
  },

  create: function (context) {
    return {
      CallExpression: function (node) {
        if (node.callee.name === 'fetch' || node.callee.property?.name === 'post') {
          // 检查 fetch 或 axios.post 请求
          const options = node.arguments[1];

          if (options && options.type === 'ObjectExpression') {
            let hasCsrfToken = false;
            for (const property of options.properties) {
              if (property.key.name === 'headers' && property.value.type === 'ObjectExpression') {
                for (const headerProperty of property.value.properties) {
                  if (headerProperty.key.value === 'X-CSRF-Token' || headerProperty.key.name === 'X-CSRF-Token') {
                    hasCsrfToken = true;
                    break;
                  }
                }
              }
            }

            if (!hasCsrfToken) {
              context.report({
                node: node,
                message: '缺少 CSRF token,可能存在 CSRF 攻击风险',
              });
            }
          }
        }

        if (node.callee.name === 'XMLHttpRequest') {
            // 检查 XMLHttpRequest 请求
            context.report({
                node: node,
                message: '请确保 XMLHttpRequest 请求包含了 CSRF token',
            });
        }
      },
    };
  },
};

代码解读:

  • CallExpression: 访问函数调用表达式,比如 fetch('/api/data', { ... })
  • 检查 fetchaxios.post 请求的配置对象中是否包含 X-CSRF-Token 头部。
  • context.report: 报告问题,node 指定问题节点,message 指定错误信息。

使用方法:

  1. 创建 custom-eslint-rules 文件夹,将 require-csrf-token.js 放入其中。
  2. .eslintrc.js 中配置:
module.exports = {
  "plugins": ["custom-eslint-rules"],
  "rules": {
    "custom-eslint-rules/require-csrf-token": "error"
  },
  "rulesDirectory": ["custom-eslint-rules/"]
};

案例四:禁止使用高危函数

有些 JS 函数本身就存在安全风险,比如 evalFunction 等。咱们可以写一个 ESLint 规则,禁止使用这些高危函数。

// custom-eslint-rules/no-dangerous-functions.js

module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '禁止使用 eval 和 Function 等高危函数',
      category: 'Security',
      recommended: 'error',
    },
    fixable: null,
    schema: [],
  },

  create: function (context) {
    return {
      Identifier: function (node) {
        if (node.name === 'eval' || node.name === 'Function') {
          context.report({
            node: node,
            message: '禁止使用 eval 和 Function 等高危函数,可能存在安全风险',
          });
        }
      },
    };
  },
};

代码解读:

  • Identifier: 访问标识符,比如变量名、函数名等。
  • 判断标识符是否是 evalFunction
  • context.report: 报告问题,node 指定问题节点,message 指定错误信息。

使用方法:

  1. 创建 custom-eslint-rules 文件夹,将 no-dangerous-functions.js 放入其中。
  2. .eslintrc.js 中配置:
module.exports = {
  "plugins": ["custom-eslint-rules"],
  "rules": {
    "custom-eslint-rules/no-dangerous-functions": "error"
  },
  "rulesDirectory": ["custom-eslint-rules/"]
};

更多可能性:规则的无限扩展

这只是几个简单的例子,你可以根据自己的安全需求,编写更复杂的规则。

安全漏洞 规则示例
目录遍历 检测代码中是否存在拼接文件路径的情况,防止攻击者访问任意文件。
SSRF 检测代码中是否存在使用用户输入作为 URL 的情况,防止攻击者利用服务器发送恶意请求。
反序列化 检测代码中是否存在使用 JSON.parse 解析用户输入的情况,防止攻击者构造恶意对象,执行任意代码。

总结:安全,永无止境

静态代码分析只是安全审计的一部分,它不能解决所有问题。但是,它可以帮助我们发现潜在的风险,提高代码的安全性。

记住,安全是一个持续的过程,我们需要不断学习新的安全知识,更新我们的规则,才能更好地保护我们的代码。

友情提示:

  • 编写规则时,要充分考虑各种情况,避免误报。
  • 定期更新规则,以应对新的安全漏洞。
  • 将静态代码分析集成到 CI/CD 流程中,自动化安全审计。

好了,今天的讲座就到这里。希望大家能够学以致用,打造自己的专属“安全卫士”,让我们的代码更加安全可靠! 感谢各位的收看!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注