介绍
面试题经常会提到一个问题,就是前端安全问题哪些,我们基本上都回答上来以下几个:
- XSS脚本攻击,利用网站漏洞,注入非法脚本
- CSRF跨站请求伪造,攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求
- 运营商劫持,一般是经过某些运营商DNS网关后,在外层套入iframe,然后实现页面劫持
但是在前端安全中,还有一个问题就是 :JS原型链污染
, 那么是原型链污染怎么回事,请看下面详细介绍。
去年有一个很邪恶的js库叫Evil.js,名字还取得很lodash,其中主要就是利用原型prototype进行重写项目中常用的方法,如:
- Array.map 有5%概率会丢失最后一个元素,
- 当数组长度可以被7整除时,Array.includes 永远返回false。
所以学会如何防御项目内被攻击,了解更多JS原型安全知识很重要,起码周末不用调试半天问题,才发现项目中代码被恶意写入。
在看这篇文章,请确保自己对JS原型和原型链有一定了解,如果忘记或缺乏了解的话,可以看我之前的文案从null、undefined、NaN的区别了解JS的原型链再次复习和学习一下。
原型链污染
是什么
下面通过一段代码,我们能够快速了解原型链是如何污染的?
const foo = { bar: 1 }; // 如果这里将foo.__proto__改掉 foo.__proto__.bar = 2 console.log(foo.bar); // 这里正常输出 1 // 新声明一个 const zoo = {}; console.log(zoo.bar); // 这里错误输出 2
从上述代码可以概括一下,原型链攻击是:
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
攻击场景
其实我们主要看哪些场景会允许代码设置__proto__
?主要有以下两种:
- 对象merge
- 对象clone
- Node.js的construtor
merge demo案例
一般实现对象的merge代码实现如下:
function merge(target, source) { for (let key in source) { if (key in source && key in target) { merge(target[key], source[key]) } else { target[key] = source[key] } } } // 运行代码 let o1 = {} let o2 = {a: 1, "__proto__": {b: 2}} merge(o1, o2) console.log(o1.a, o1.b) o3 = {} console.log(o3.b) // 发现原型链没有被污染
这是为什么呢?因为声明变量的时候,"_proto"并不是一个key,即使修改也不会改变Object的原型。
那么如何做到"_proto"成为一个key,可以将demo代码调整一下,如:
let o1 = {} let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}') merge(o1, o2) console.log(o1.a, o1.b) o3 = {} console.log(o3.b) // 输出2
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。如: lodash.merge
。
实际应用问题
Code-Breaking 2018 Thejs 分析
// ... const lodash = require('lodash') // ... app.engine('ejs', function (filePath, options, callback) { // define the template engine fs.readFile(filePath, (err, content) => { if (err) return callback(new Error(err)) let compiled = lodash.template(content) let rendered = compiled({...options}) return callback(null, rendered) }) }) //... app.all('/', (req, res) => { let data = req.session.data || {language: [], category: []} if (req.method == 'POST') { data = lodash.merge(data, req.body) req.session.data = data } res.render('index', { language: data.language, category: data.category }) })
题目是利用lodash.template
+ lodash.merge
两个方法实现部分功能:
- 用户提交的信息,用merge方法合并到session里,session里最终保存你提交的所有信息
- 然后利用merge方法注入原型污染
- 最后利用template中的方法,完成整个网站污染
整个案例其实比较重要的一点就是,提交参数的时候序列化提交参数是否有做防御,如何没有的话那么很容易就中招了。
如何防御
分不同情况做防御:
引入npm包导致的
- 项目代码扫描,主要是针对构建好的代码进行扫描,是否
Evil.js
修改内置原型的情况进行告警提示 - 禁止修改原型,如:使用
Object.freeze
针对node.js
- 针对node服务端接口参数进行序列化的时候,禁止非法
key
值传入 - 禁止修改原型,如:使用
Object.freeze
- 采用
Object.create(null)
创建对象,避免直接使用{}
- 使用
map
数据类型创建 - 不对参数做
JSON.parse
转换,采用封装后的反序列化方法 - 尽量采用安全的
merge
或clone
库方法