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

大家好,我是今天的主讲人,代号“代码猎手”,专门负责在代码的犄角旮旯里揪出安全隐患。今天咱们就来聊聊如何打造属于咱们自己的静态代码分析规则,让 ESLint 和 SonarJS 成为咱们的“安全卫士”。

开场白:为啥要自己动手?

市面上已经有很多静态代码分析工具,比如 ESLint 和 SonarJS,它们自带的规则已经很强大了。那为啥还要自己动手开发自定义规则呢?原因很简单:

  1. 定制化需求: 不同的项目有不同的安全要求。比如,金融领域的项目可能对数据精度要求极高,需要避免浮点数计算的精度问题;物联网项目可能需要特别关注内存泄露和资源释放的问题。通用规则可能无法覆盖所有这些特定场景。

  2. 漏洞挖掘: 新型漏洞层出不穷,静态分析工具的规则更新速度可能跟不上漏洞的出现速度。自己开发规则,可以更快地针对最新漏洞进行防御。

  3. 代码风格统一: 除了安全问题,自定义规则还可以用于强制执行特定的代码风格,提高代码可读性和可维护性。

总之,自己开发静态分析规则,就像给自己的项目量身定制一套铠甲,防御力 MAX!

第一部分:ESLint 自定义规则开发

ESLint 是 JavaScript 代码检查的利器,它的规则基于 AST(Abstract Syntax Tree,抽象语法树)进行分析。所以,要开发 ESLint 规则,首先要了解 AST。

1. AST 基础

AST 是源代码的树状表示。每个节点代表代码中的一个语法结构,比如变量声明、函数调用、表达式等。我们可以使用 AST Explorer 这个工具来查看 JavaScript 代码的 AST 结构。

例如,对于以下代码:

const x = 1 + 2;
console.log(x);

AST Explorer 会显示出类似这样的结构(简化版):

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "x"
          },
          "init": {
            "type": "BinaryExpression",
            "operator": "+",
            "left": {
              "type": "Literal",
              "value": 1
            },
            "right": {
              "type": "Literal",
              "value": 2
            }
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "MemberExpression",
          "object": {
            "type": "Identifier",
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "name": "log"
          }
        },
        "arguments": [
          {
            "type": "Identifier",
            "name": "x"
          }
        ]
      }
    }
  ]
}

可以看到,AST 将代码分解成了各种节点,我们可以通过遍历 AST 来找到特定的代码模式。

2. ESLint 规则结构

一个 ESLint 规则通常包含以下几个部分:

  • meta: 规则的元数据,包括规则的描述、类型(problem/suggestion/layout)、修复方法等。
  • create: 一个函数,接收 context 对象作为参数,返回一个对象,其中包含对 AST 节点类型的访问器(visitor)。

下面是一个简单的 ESLint 规则示例,用于禁止使用 console.log

// my-custom-rule.js
module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '禁止使用 console.log',
      category: 'Possible Errors',
      recommended: 'error',
    },
    fixable: null, // 如果可以自动修复,设置为 'code'
    schema: [], // 规则的配置项
  },
  create: function (context) {
    return {
      CallExpression: function (node) {
        if (node.callee.type === 'MemberExpression' &&
            node.callee.object.type === 'Identifier' &&
            node.callee.object.name === 'console' &&
            node.callee.property.type === 'Identifier' &&
            node.callee.property.name === 'log') {
          context.report({
            node: node,
            message: '禁止使用 console.log,请使用更高级的日志方法!',
          });
        }
      },
    };
  },
};
  • meta.type: problem 表示这是一个潜在的问题,suggestion 表示可以提供修改建议,layout 表示代码风格问题。
  • meta.docs: 包含规则的描述、分类和推荐级别。
  • meta.fixable: 如果规则可以自动修复,设置为 'code'
  • meta.schema: 定义规则的配置项,允许用户自定义规则的行为。
  • create: 返回的对象中,CallExpression 是一个 AST 节点类型的访问器。当 ESLint 遍历 AST 时,遇到 CallExpression 类型的节点,就会调用这个函数。
  • context.report: 用于报告问题,接收一个对象,包含节点信息和错误消息。

3. 如何使用自定义规则

  1. 创建规则文件: 将规则代码保存为一个 JavaScript 文件,比如 my-custom-rule.js

  2. 配置 ESLint:.eslintrc.js.eslintrc.json 文件中配置 ESLint,启用自定义规则。

    // .eslintrc.js
    module.exports = {
      "plugins": [
        "my-custom-plugin" // 插件名称,可以自定义
      ],
      "rules": {
        "my-custom-plugin/my-custom-rule": "error" // 规则名称,'error' 表示这是一个错误
      },
      "extends": [
        "eslint:recommended"
      ],
      "env": {
        "browser": true,
        "node": true,
        "es6": true
      },
      "parserOptions": {
        "ecmaVersion": 2018,
        "sourceType": "module"
      }
    };

    需要注意的是,上面的配置需要一个插件,插件需要将自定义规则暴露出来。创建一个文件 eslint-plugin-my-custom-plugin/index.js

    // eslint-plugin-my-custom-plugin/index.js
    module.exports = {
        rules: {
            "my-custom-rule": require("./my-custom-rule")
        }
    };
  3. 运行 ESLint: 运行 ESLint,它会自动加载并执行自定义规则。

4. 进阶:自动修复

如果规则可以自动修复,可以在 meta 中设置 fixable: 'code',并在 context.report 中提供 fix 函数。

下面是一个示例,用于将单引号字符串替换为双引号字符串:

// quote-style.js
module.exports = {
  meta: {
    type: 'layout',
    docs: {
      description: '强制使用双引号字符串',
      category: 'Stylistic Issues',
      recommended: 'warn',
    },
    fixable: 'code',
    schema: [],
  },
  create: function (context) {
    return {
      Literal: function (node) {
        if (typeof node.value === 'string' && node.value.includes("'")) {
          context.report({
            node: node,
            message: '应该使用双引号字符串',
            fix: function (fixer) {
              return fixer.replaceText(node, `"${node.value}"`);
            },
          });
        }
      },
    };
  },
};

fix 函数接收 fixer 对象作为参数,fixer 对象提供了一系列方法用于修改代码,比如 replaceTextinsertTextBeforeinsertTextAfter 等。

5. 实战案例:防止 XSS 漏洞

假设我们需要防止 XSS 漏洞,禁止直接将用户输入的数据渲染到 DOM 中。我们可以开发一个 ESLint 规则来检查 innerHTML 属性的赋值。

// no-innerhtml.js
module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '禁止使用 innerHTML 属性,防止 XSS 漏洞',
      category: 'Security',
      recommended: 'error',
    },
    fixable: null,
    schema: [],
  },
  create: function (context) {
    return {
      AssignmentExpression: function (node) {
        if (node.left.type === 'MemberExpression' &&
            node.left.property.type === 'Identifier' &&
            node.left.property.name === 'innerHTML') {
          context.report({
            node: node,
            message: '禁止使用 innerHTML 属性,请使用更安全的 API,例如 textContent 或 setAttribute。',
          });
        }
      },
    };
  },
};

这个规则会检查所有赋值表达式,如果左侧是 innerHTML 属性,就会报告一个错误。

第二部分:SonarJS 自定义规则开发

SonarJS 是 SonarQube 的 JavaScript 插件,用于静态代码分析。SonarJS 规则的开发方式与 ESLint 类似,也基于 AST 进行分析。

1. SonarJS 规则结构

一个 SonarJS 规则通常包含以下几个部分:

  • key: 规则的唯一标识符。
  • name: 规则的名称。
  • description: 规则的描述。
  • tags: 规则的标签,用于分类。
  • severity: 规则的严重程度(info/minor/major/critical/blocker)。
  • type: 规则的类型(code smell/bug/vulnerability)。
  • create: 一个函数,接收 context 对象作为参数,返回一个对象,其中包含对 AST 节点类型的访问器(visitor)。

下面是一个简单的 SonarJS 规则示例,用于禁止使用 alert 函数:

// no-alert.js
module.exports = {
  key: 'no-alert',
  name: '禁止使用 alert 函数',
  description: '禁止使用 alert 函数,因为它会阻塞浏览器',
  tags: ['security', 'performance'],
  severity: 'major',
  type: 'code smell',
  create: function (context) {
    return {
      CallExpression: function (node) {
        if (node.callee.type === 'Identifier' && node.callee.name === 'alert') {
          context.report({
            node: node,
            message: '禁止使用 alert 函数,请使用更友好的提示方式!',
          });
        }
      },
    };
  },
};
  • key: 规则的唯一标识符,必须是唯一的。
  • severity: 规则的严重程度,major 表示这是一个主要问题。
  • type: 规则的类型,code smell 表示这是一个代码异味。
  • context.report: 与 ESLint 类似,用于报告问题。

2. 如何使用自定义规则

  1. 创建规则文件: 将规则代码保存为一个 JavaScript 文件,比如 no-alert.js

  2. 创建插件: 创建一个插件,将自定义规则暴露出来。

    // my-custom-plugin.js
    module.exports = {
      id: 'my-custom-plugin',
      name: 'My Custom Plugin',
      description: '自定义 SonarJS 规则',
      rules: {
        'no-alert': require('./no-alert'),
      },
    };
  3. 配置 SonarQube: 将插件添加到 SonarQube 中。具体步骤如下:

    • 将插件文件(my-custom-plugin.js)打包成一个 JAR 文件。
    • 将 JAR 文件复制到 SonarQube 的 extensions/plugins 目录下。
    • 重启 SonarQube。
  4. 配置分析器: 在 SonarQube 的项目中配置分析器,启用自定义规则。

    • 在 SonarQube 的项目中,点击 "Quality Profiles"。
    • 选择 "SonarJS" 质量配置。
    • 在 "Rules" 页面,搜索自定义规则的 key(例如 no-alert)。
    • 启用该规则。
  5. 运行分析: 运行 SonarQube 分析,它会自动加载并执行自定义规则。

3. 实战案例:防止 SQL 注入

假设我们需要防止 SQL 注入漏洞,禁止直接将用户输入的数据拼接到 SQL 语句中。我们可以开发一个 SonarJS 规则来检查字符串拼接操作。

// no-sql-injection.js
module.exports = {
  key: 'no-sql-injection',
  name: '防止 SQL 注入',
  description: '禁止直接将用户输入的数据拼接到 SQL 语句中,防止 SQL 注入漏洞',
  tags: ['security', 'sql'],
  severity: 'critical',
  type: 'vulnerability',
  create: function (context) {
    return {
      BinaryExpression: function (node) {
        if (node.operator === '+') {
          // 简单判断,如果拼接的字符串包含 "SELECT", "INSERT", "UPDATE", "DELETE" 等关键字,就报告一个问题
          const left = node.left.value || (node.left.type === 'Identifier' ? node.left.name : '');
          const right = node.right.value || (node.right.type === 'Identifier' ? node.right.name : '');

          const concatenatedString = String(left) + String(right);

          if (concatenatedString.toLowerCase().includes('select') ||
              concatenatedString.toLowerCase().includes('insert') ||
              concatenatedString.toLowerCase().includes('update') ||
              concatenatedString.toLowerCase().includes('delete')) {
            context.report({
              node: node,
              message: '禁止直接将用户输入的数据拼接到 SQL 语句中,请使用参数化查询!',
            });
          }
        }
      },
    };
  },
};

这个规则会检查所有字符串拼接操作,如果拼接后的字符串包含 SQL 关键字,就会报告一个问题。

第三部分:规则开发技巧和注意事项

  1. 善用 AST Explorer: AST Explorer 是开发静态分析规则的必备工具,可以帮助我们快速了解代码的 AST 结构。

  2. 逐步迭代: 不要试图一次性开发出完美的规则。可以先从简单的规则开始,逐步迭代和完善。

  3. 编写单元测试: 为规则编写单元测试,确保规则能够正确地检测到问题,并且不会误报。

  4. 考虑性能: 静态分析工具的性能非常重要。尽量避免复杂的 AST 遍历和计算,提高规则的执行效率。

  5. 提供清晰的错误消息: 错误消息应该清晰明了,告诉开发者问题是什么,以及如何解决。

  6. 注意误报率: 尽量降低规则的误报率,避免给开发者带来不必要的困扰。

  7. 持续维护: 随着代码库的演进,规则可能需要进行调整和更新。持续维护规则,确保它们能够适应新的代码模式和安全威胁。

表格总结:ESLint vs SonarJS

特性 ESLint SonarJS
主要用途 JavaScript 代码风格检查和问题发现 代码质量管理和安全审计
规则类型 代码风格、潜在错误、安全问题 代码异味、缺陷、漏洞
集成方式 集成到编辑器、构建工具等 集成到 SonarQube 平台
适用场景 本地开发环境、持续集成环境 大型项目、团队协作、代码质量监控
规则配置 .eslintrc.js.eslintrc.json 文件 SonarQube 平台
错误报告 控制台输出、编辑器提示 SonarQube 平台、报告
社区支持 非常活跃 相对活跃

结语:安全无小事,代码需谨慎

静态代码分析是提高代码质量和安全性的重要手段。通过自己开发自定义规则,我们可以更好地满足项目的特定需求,及时发现和修复潜在的安全隐患。希望今天的讲座能够帮助大家更好地理解和应用静态代码分析技术,打造更安全、更可靠的代码。记住,安全无小事,代码需谨慎!

祝大家编码愉快,安全常伴!

发表回复

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