前端安全-JS原型链污染是怎么回事

418 阅读4分钟

介绍

面试题经常会提到一个问题,就是前端安全问题哪些,我们基本上都回答上来以下几个:

  • 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转换,采用封装后的反序列化方法
  • 尽量采用安全的mergeclone库方法

参考资料