大家好,我是今天的主讲人,代号“代码猎手”,专门负责在代码的犄角旮旯里揪出安全隐患。今天咱们就来聊聊如何打造属于咱们自己的静态代码分析规则,让 ESLint 和 SonarJS 成为咱们的“安全卫士”。
开场白:为啥要自己动手?
市面上已经有很多静态代码分析工具,比如 ESLint 和 SonarJS,它们自带的规则已经很强大了。那为啥还要自己动手开发自定义规则呢?原因很简单:
-
定制化需求: 不同的项目有不同的安全要求。比如,金融领域的项目可能对数据精度要求极高,需要避免浮点数计算的精度问题;物联网项目可能需要特别关注内存泄露和资源释放的问题。通用规则可能无法覆盖所有这些特定场景。
-
漏洞挖掘: 新型漏洞层出不穷,静态分析工具的规则更新速度可能跟不上漏洞的出现速度。自己开发规则,可以更快地针对最新漏洞进行防御。
-
代码风格统一: 除了安全问题,自定义规则还可以用于强制执行特定的代码风格,提高代码可读性和可维护性。
总之,自己开发静态分析规则,就像给自己的项目量身定制一套铠甲,防御力 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. 如何使用自定义规则
-
创建规则文件: 将规则代码保存为一个 JavaScript 文件,比如
my-custom-rule.js
。 -
配置 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") } };
-
运行 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
对象提供了一系列方法用于修改代码,比如 replaceText
、insertTextBefore
、insertTextAfter
等。
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. 如何使用自定义规则
-
创建规则文件: 将规则代码保存为一个 JavaScript 文件,比如
no-alert.js
。 -
创建插件: 创建一个插件,将自定义规则暴露出来。
// my-custom-plugin.js module.exports = { id: 'my-custom-plugin', name: 'My Custom Plugin', description: '自定义 SonarJS 规则', rules: { 'no-alert': require('./no-alert'), }, };
-
配置 SonarQube: 将插件添加到 SonarQube 中。具体步骤如下:
- 将插件文件(
my-custom-plugin.js
)打包成一个 JAR 文件。 - 将 JAR 文件复制到 SonarQube 的
extensions/plugins
目录下。 - 重启 SonarQube。
- 将插件文件(
-
配置分析器: 在 SonarQube 的项目中配置分析器,启用自定义规则。
- 在 SonarQube 的项目中,点击 "Quality Profiles"。
- 选择 "SonarJS" 质量配置。
- 在 "Rules" 页面,搜索自定义规则的
key
(例如no-alert
)。 - 启用该规则。
-
运行分析: 运行 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 关键字,就会报告一个问题。
第三部分:规则开发技巧和注意事项
-
善用 AST Explorer: AST Explorer 是开发静态分析规则的必备工具,可以帮助我们快速了解代码的 AST 结构。
-
逐步迭代: 不要试图一次性开发出完美的规则。可以先从简单的规则开始,逐步迭代和完善。
-
编写单元测试: 为规则编写单元测试,确保规则能够正确地检测到问题,并且不会误报。
-
考虑性能: 静态分析工具的性能非常重要。尽量避免复杂的 AST 遍历和计算,提高规则的执行效率。
-
提供清晰的错误消息: 错误消息应该清晰明了,告诉开发者问题是什么,以及如何解决。
-
注意误报率: 尽量降低规则的误报率,避免给开发者带来不必要的困扰。
-
持续维护: 随着代码库的演进,规则可能需要进行调整和更新。持续维护规则,确保它们能够适应新的代码模式和安全威胁。
表格总结:ESLint vs SonarJS
特性 | ESLint | SonarJS |
---|---|---|
主要用途 | JavaScript 代码风格检查和问题发现 | 代码质量管理和安全审计 |
规则类型 | 代码风格、潜在错误、安全问题 | 代码异味、缺陷、漏洞 |
集成方式 | 集成到编辑器、构建工具等 | 集成到 SonarQube 平台 |
适用场景 | 本地开发环境、持续集成环境 | 大型项目、团队协作、代码质量监控 |
规则配置 | .eslintrc.js 或 .eslintrc.json 文件 |
SonarQube 平台 |
错误报告 | 控制台输出、编辑器提示 | SonarQube 平台、报告 |
社区支持 | 非常活跃 | 相对活跃 |
结语:安全无小事,代码需谨慎
静态代码分析是提高代码质量和安全性的重要手段。通过自己开发自定义规则,我们可以更好地满足项目的特定需求,及时发现和修复潜在的安全隐患。希望今天的讲座能够帮助大家更好地理解和应用静态代码分析技术,打造更安全、更可靠的代码。记住,安全无小事,代码需谨慎!
祝大家编码愉快,安全常伴!