JavaScript 安全 101
该博客文章最初发表于 Tes Engineering博客。
我最近完成了Marcin Hoppe的《JavaScript 安全:最佳实践》课程,想分享一些我学到的关于如何编写更安全的 JavaScript 代码的关键实践经验。 除了阅读这篇博客,我也强烈推荐大家完成这门课程。它简洁易懂,而且易于实践!
JavaScript 威胁环境
值得注意的是,存在两种不同的威胁环境:客户端 JavaScript 和服务器端 JavaScript。对于客户端 JavaScript,浏览器在低信任度和高度受限的基础上运行,这是必然的,因为它会通过用户浏览网页来处理来自不受控制来源的 JavaScript。
相比之下,对于服务器端 JavaScript,Node.js 在高信任度和特权的基础上运行,因为它是受控来源(即由工程团队编写的代码),并且在运行时不会更改。Node.js安全路线图
中对这些不同的威胁环境进行了更详细的总结,在编写 JavaScript 时务必牢记这一差异。
JavaScript 的动态特性一方面使其用途极其广泛,另一方面也带来了许多安全隐患。以下是 JavaScript 中的三个主要隐患以及如何避免它们。
1. 滥用比较和转换
简而言之:
JavaScript 拥有动态类型系统,这可能会带来一些危险但可以避免的后果。使用 JavaScript严格模式可以帮助避免诸如松散比较之类的陷阱。
一些例子...
NaN、Null 和 undefined
自动转换可能会导致执行意外的代码:
console.log(typeof NaN) // number
console.log(typeof null) // object
console.log(typeof undefined) // undefined
例如,此calculatingStuff
函数依赖于输入为数字。没有任何验证来防止输入为NaN
,该函数仍然运行,因为NaN
被归类为数字。
const calculatingStuff = (num) => {
return num * 3;
};
console.log(calculatingStuff(NaN)) // NaN
为了避免自动转换中出现意外行为,设置保护条款和错误处理机制至关重要。例如,在这个版本的 中,calculatingStuffv2
如果输入为 ,我们会抛出错误NaN
。
const calculatingStuffv2 = (num) => {
if (isNaN(num)) {
return new Error('Not a number!')
}
return num * 3;
};
console.log(calculatingStuffv2(NaN)) // Error: Not a number!
console.log(calculatingStuffv2(undefined)) // Error: Not a number!
console.log(calculatingStuffv2(null)) // 0
console.log(calculatingStuffv2(2)) // 6
也isNaN()
能防范 undefined,但无法防范null
。与 JavaScript 中的所有内容一样,有很多方法可以编写检查来防范NaN
、null
和undefined
。
一个更可靠的“全部捕获”方法是检查真值,因为所有这些值都是假值,它们总是会返回错误:
const calculatingStuffv2 = (num) => {
if (!num) {
return new Error('Not a number!')
}
return num * 3;
};
console.log(calculatingStuffv2(NaN)) // Error: Not a number!
console.log(calculatingStuffv2(undefined)) // Error: Not a number!
console.log(calculatingStuffv2(null)) // // Error: Not a number!
console.log(calculatingStuffv2(2)) // 6
宽松的比较
松散比较是代码可能被意外执行的另一种方式:
const num = 0;
const obj = new String('0');
const str = '0';
console.log(num == obj); // true
console.log(num == str); // true
console.log(obj == str); // true
使用严格比较===
可以排除意外副作用的可能性,因为它总是认为不同类型的操作数是不同的。
const num = 0;
const obj = new String('0');
const str = '0';
console.log(num === obj); // false
console.log(num === str); // false
console.log(obj === str); // false
2. 动态执行代码的注入攻击
TLDR;
务必在应用程序中使用数据之前始终验证数据,并避免将字符串作为参数传递给可以动态执行代码的 JavaScript 函数。
一些例子...
评估()
正如mdn 文档中所述,eval“以调用者的权限执行传递的代码”。
例如,如果向 eval 传递了未经验证的用户输入(其中包含恶意代码),这可能会变得非常危险。
eval('(' + '<script type='text/javascript'>some malicious code</script>' + '(');
浏览器 API 的不安全变体
setTimeout和setInterval都有一个可选语法,可以传递字符串而不是函数。
window.setTimeout('<script type='text/javascript'>some malicious code</script>', 2*1000);
正如eval()
示例所示,这将导致在运行时执行恶意代码。可以通过始终使用传递函数作为参数的语法来避免这种情况。
3.原型污染攻击
简而言之:
每个 JavaScript 对象都有一个原型链,它是可变的,可以在运行时更改。可以通过以下方式防止这种情况:
- 冻结原型以防止添加或修改新属性
- 创建没有原型的对象
- 优先使用Map而不是普通
{}
对象
一些例子...
toString
下面是一个通过改变原型中的函数值来执行恶意脚本的例子。
let cutePuppy = {name: "Barny", breed: "Beagle"}
cutePuppy.__proto__.toString = ()=>{<script type='text/javascript'>some malicious code</script>}
减轻这种风险的几种方法是在启动新对象时要小心,要么创建它们并删除原型,要么冻结原型,要么使用Map 对象。
// remove
let cutePuppyNoPrototype = Object.create(null, {name: "Barny", breed: "Beagle"})
// freeze
const proto = cutePuppyNoPrototype.prototype;
Object.freeze(proto);
// Map
let puppyMap = new Map()
cutePuppyNoPrototype.set({name: "Barny", breed: "Beagle"})
原型继承是一个被低估的威胁,因此绝对值得考虑这一点,以防止 JavaScript 以各种方式被利用。
工具
最后,除了意识到 JavaScript 的这些缺陷之外,您还可以使用一些工具在开发过程中获取早期反馈。务必考虑您编写的 JavaScript 以及通过依赖项引入的第三方 JavaScript 的安全问题。
以下是Awesome Node.js security和Guidesmiths Cybersecurity 手册中列出的一些出色的静态代码分析 (SAST) 工具的亮点。
在你的代码中
- 编写 JavaScript 时始终
use strict
处于开发模式 - 使用 linter,例如eslint可以通过编辑规则来配置,以防止我们上面探讨的一些陷阱:
"rules": {
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
}
- 在文本编辑器中使用安全插件,例如eslint-plugin-security
在你的 JavaScript 依赖项代码中
- 使用npm audit检查已知漏洞
- 使用lockfile lint检查
package-lock.json
通常不被审查的更改 - 使用trust but verify将 npm 包与其源存储库进行比较,以确保生成的工件相同