改动背景

上一篇里,我主要补齐了 Python XSS 检测。到那一轮结束后,Python 侧基本已经覆盖了任务书里的四类核心风险:

SQL 注入
XSS
危险函数调用
硬编码密钥

这一篇开始往 JavaScript 方向扩展。

不过这次不是把 JavaScript 做成和 Python 一样深的分析器。Python 仍然是当前项目里的主线,前面已经做了 AST、flow analyzer、函数调用传播、返回值追踪、作用域处理、Web 入口上下文提示等能力。

JavaScript 这一轮的目标更明确:用轻量规则覆盖四类核心风险的常见写法,让 JS 侧也能进入统一的 detector / engine / evaluator 链路,从而证明当前架构可以扩展到第二种语言。

一、为什么这一篇做 JavaScript 轻量扩展

前面几篇主要都围绕 Python 展开。SQL 注入、危险函数、硬编码密钥、XSS 这几类风险已经陆续接入了统一分析链路,其中 SQL 注入、危险函数、XSS 还做了不同程度的数据流分析。

由于我们计划做多语言的扩展,所以这一篇开始接入 JavaScript。

这里需要先明确一点:JS 不是这一阶段的深度分析主线。它更像是一个轻量验证版,用来回答一个问题:

当前 detector / engine / evaluator 架构能不能自然扩展到第二种语言?

所以这次 JS 侧主要做三件事:

覆盖四类核心风险的常见写法
接入统一 detector 链路
保持输出结构和 Python 一致

这样既能证明多语言扩展是可行的,也不会把 JavaScript 扩展成另一条很重的静态分析主线。

二、本轮新增了什么

这次新增了 JavaScript 四类核心风险的轻量 detector:

JavaScript SQLInjection
JavaScript XSS
JavaScript HardcodedSecret
JavaScript DangerousFunction

整体仍然接入之前的结构:

rules
  -> engine
  -> evaluator
  -> detector
  -> registry

也就是说,JavaScript 没有单独另起一套流程,而是放进了现有分析链路。

后端 /analyze 主流程也不需要大改。请求里如果是:

{
  "language": "python"
}

就走 Python detector;如果是:

{
  "language": "javascript"
}

就走 JavaScript detector。

这样插件展示、结果转换、后续解释功能都可以继续复用原来的结构。

三、为什么 JS 这次采用行扫描和正则规则

这次 JavaScript 没有引入完整 AST parser,而是采用了文本级的轻量分析方式。

当前整体流程大概是:

源码文本
  -> 按行读取
  -> 去掉 // 和 /* */ 注释
  -> 用正则识别 source / sink / 赋值 / 字符串
  -> 维护少量变量状态
  -> 生成风险候选

它属于“文本级 + 简单变量级”的分析。

比如下面这段代码:

const id = req.query.id;
const sql = `SELECT * FROM users WHERE id = ${id}`;
db.query(sql);

当前逻辑大概会理解成:

id 来自 req.query.id,是 taint
sql 是包含 id 的动态 SQL
sql 进入 db.query
=> SQLInjection

如果使用 AST,分析器会先把 JavaScript 代码解析成语法树。比如变量声明、成员访问、函数调用、模板字符串、函数参数都会变成明确的语法节点。这样更适合做跨行表达式、复杂函数传播、作用域遮蔽、import / require 解析等更深层分析。

但这个项目里,JS 侧没有选择这条路线,主要是因为定位不同。

JavaScript 生态比 Python 更分散:既有浏览器 DOM,也有 Node.js 后端;既有 CommonJS 的 require,也有 ESM 的 import;还可能涉及 React、Vue、Express、Koa、回调、异步函数、对象链式调用等写法。

如果一开始就引入完整 AST,并继续处理这些语法和框架场景,JS 分析很容易变成另一条很重的主线。

而当前项目的重点是 Python 深度分析,JS 侧主要承担多语言验证。因此这次选择行扫描、正则匹配和简单变量状态,覆盖高频、确定性强、容易解释的风险写法。

简单说,JS 不是 Python 深度分析的复制版,而是为了多语言验证设计的轻量规则扫描版。

四、JavaScript SQL 注入

JS SQL 注入的判断思路还是:

source -> dynamic SQL -> sink

也就是说,不是看到 query(...) 就直接报,而是尽量确认用户输入进入了动态 SQL 字符串,并且这个 SQL 又进入数据库执行点。

目前支持一些常见输入来源:

req.query.id
req.params.id
req.body.name
req.headers["x-user"]
ctx.query.id
event.queryStringParameters.id

模板字符串 SQL 可以识别:

const id = req.query.id;
const sql = `SELECT * FROM users WHERE id = ${id}`;
db.query(sql);

字符串拼接也可以识别:

const name = req.body.name;
connection.query("SELECT * FROM users WHERE name = '" + name + "'");

常见数据库执行点也做了轻量覆盖:

db.query(sql);
connection.execute(sql);
db.raw(sql);
db.run(sql);
db.all(sql);
db.get(sql);
prisma.$queryRawUnsafe(sql);
prisma.$executeRawUnsafe(sql);

其中 run / all / get 主要覆盖 SQLite 一类写法,$queryRawUnsafe / $executeRawUnsafe 主要覆盖 Prisma 中比较明确的 unsafe raw SQL 调用。

参数化查询会放行:

const id = req.query.id;
db.query("SELECT * FROM users WHERE id = ?", [id]);

这里虽然 id 仍然来自用户输入,但没有直接拼进 SQL 字符串里,所以当前不会报 SQL 注入。

这部分目前是轻量变量级传播,不做复杂函数传播。

五、JavaScript XSS

JS XSS 的判断思路还是:

source -> HTML build -> HTML sink

不过 JS XSS 和 Python XSS 有一点区别。Python XSS 更偏服务端返回 HTML;JS XSS 既可能出现在服务端响应,也可能出现在浏览器 DOM 写入里。

常见输入来源包括:

req.query.q
req.body.name
location.search
document.cookie
localStorage.getItem("x")

DOM HTML 写入可以识别:

const q = req.query.q;
element.innerHTML = `<h1>${q}</h1>`;

也支持这些 HTML sink:

element.outerHTML = `<div>${q}</div>`;
document.write(`<p>${q}</p>`);
element.insertAdjacentHTML("beforeend", `<span>${q}</span>`);

这里需要稍微区分一下:对于 innerHTMLdocument.write 这类 DOM sink,sink 本身会把字符串当作 HTML 解析;对于 res.send 这类服务端响应,则更关注用户输入是否已经参与 HTML 字符串构造。

所以服务端响应场景更典型的写法是:

const q = req.query.q;
const html = `<h1>${q}</h1>`;

res.send(html);
res.write(html);
res.end(html);
reply.send(html);

这次还补了一些前端里比较常见的 jQuery 风格写法:

const html = `<span>${q}</span>`;

$("#target").html(html);
$(".box").append(html);
$(".box").prepend(html);
$(".box").before(html);
$(".box").after(html);

React 里比较明确的 HTML 注入点也会识别:

const html = document.cookie;
const node = <div dangerouslySetInnerHTML={{ __html: html }} />;

如果经过转义或净化,会尽量放行:

const safe = escapeHtml(q);
element.innerHTML = safe;

const clean = DOMPurify.sanitize(q);
element.innerHTML = clean;

目前会识别的安全写法包括:

escapeHtml
DOMPurify.sanitize
sanitizeHtml
_.escape

这部分和 Python XSS 的原则一致:尽量确认用户输入真的进入了 HTML 输出位置,而不是只看某个函数名。

六、JavaScript 硬编码密钥

硬编码密钥这类风险本身不太依赖复杂数据流,所以 JS 侧和 Python 侧的思路比较接近:

可疑变量名 / 字段名 + 字符串字面量

例如变量赋值:

const apiKey = "sk-live-123";
const password = "123456";
const token = "prod-token-123";

对象字段也支持:

const config = {
  clientSecret: "prod-secret-123"
};

下标赋值也支持:

config["token"] = "prod-token-123";

JavaScript 里经常会出现 camelCase 命名,所以类似 apiKeyclientSecret 这类字段名也会被识别。

同时也做了一些误报控制。

比如下面这些更像字段说明、环境变量名或占位符:

const apiKeyName = "OPENAI_API_KEY";
const tokenType = "Bearer";
const token = "your_token";
const secret = "dummy";

当前不会直接当成真实密钥报。

这部分重点还是控制误报:不能只要变量名里有 keytokensecret 就报,还要看它是不是明显的占位符或元数据字段。

七、JavaScript 危险函数

危险函数和 SQL 注入、XSS 不太一样。它更关注危险 API 本身,以及参数是否可能来自不可信输入。

当前支持动态执行代码:

eval(userCode);
new Function(userCode);

字符串形式的定时器也会识别:

setTimeout("alert(1)", 1000);
setInterval("doSomething()", 1000);

Node 命令执行也做了覆盖:

const cp = require("child_process");

cp.exec(cmd);
cp.execSync(cmd);
cp.spawn(cmd, [], { shell: true });

也支持一些真实项目里常见的别名写法:

const { exec } = require("child_process");
exec(cmd);

import { exec as run } from "child_process";
run(cmd);

这里加了一个轻量 alias state,用来记录 child_process 的模块别名和函数别名。

这样可以识别:

const cp = require("child_process");
cp.exec(cmd);

也可以识别:

import { exec as run } from "child_process";
run(cmd);

但它主要覆盖常见 require / import 写法,不做复杂作用域分析,也不做跨文件 import 追踪。

八、统一输出结果

虽然 JavaScript 内部实现比 Python 轻,但最终还是会转换成统一的 RiskCandidate

比如一个 JS SQL 注入结果大概会是这样:

{
  "risk_type_candidate": "SQLInjection",
  "severity": "high",
  "start_line": 3,
  "end_line": 3,
  "summary": "不可信输入进入动态 SQL 并传入数据库执行函数,存在 SQL 注入风险。",
  "needs_more_context": false,
  "evidence_chain": {
    "source_line": 1,
    "sql_build_line": 2,
    "sink_line": 3
  }
}

这样做的好处是,不管风险来自 Python 还是 JavaScript,后面都能用同一套结构处理。

插件展示层、Problems 面板、代码高亮和后续解释层,都不需要为了 JavaScript 重新设计一套结果格式。

九、当前边界

当前 JS 侧定位就是轻量多语言验证,所以边界也比较明确。

暂不支持:

完整 JavaScript AST
跨文件 import 解析
复杂函数调用传播
深度对象属性追踪
复杂框架生命周期分析

这些能力属于更重的 JavaScript 静态分析范围,不是当前项目的重点。

当前更重要的是:JS 四类核心风险能进入统一 detector 链路,结果格式和 Python 保持一致,插件和后续解释层可以统一消费。

十、测试情况

这轮测试主要确认三件事。

第一,JS 四类风险都能进入统一 detector 链路。

第二,常见危险写法可以被识别,同时参数化 SQL、escapeHtmlDOMPurify.sanitize 这类安全写法不会乱报。

第三,新增 JavaScript detector 后,原来的 result_converter 和插件编译没有被破坏。

后端测试:

cd backend
python -m pytest tests -q

插件侧也重新编译了一遍:

cd vscode-extension
npm run compile

结果都已通过。
在这里插入图片描述

十一、总结与下一步

这一轮完成的是 JavaScript 四类核心风险的轻量闭环。

它的重点不是把 JS 做成完整成熟的深度分析器,而是验证当前可插拔 detector 架构可以扩展到第二种语言,并让 JS 侧四类风险都有可运行、可测试的版本。

到这里,项目里的分工就比较清楚了:

Python:主线,承担更完整的数据流和上下文分析
JavaScript:轻量版,覆盖常见写法,验证多语言扩展能力

后续重点会放在联调、测试矩阵、文档整理和答辩材料上,而不是继续把 JavaScript 扩展成另一条重型分析主线。

更多推荐