JS原型链污染
JS原型链
JavaScript对象模型与原型链的底层结构
在现代js中,对象并非简单的键值对集合,而是基于原型链的继承体系
每个对象都拥有一个内部属性[[Prototype]],该属性指向其”原型对象”(即父级对象),从而形成一条可追溯的链式结构
__proto__和object.getprototypeof()的关系
__proto__是非标准的属性,但在实际开发和安全研究中被广泛使用,等价于Object.getpeototypeof()方法返回的结果
1 | |
- __proto__是 实例化对象的内部属性,用于访问其原型链的上一级
- Object.getPrototypeOf(obj)是官方推荐的标准方法,用于获取任意对象的原型
- 两者功能相同,但__proto__可能被某些框架或沙箱环境拦截或禁用,因此建议优先使用getPrototypeOf进行程序化分析
原型链的遍历顺序与终止条件
当访问对象的某个属性时,引擎会按照以下顺序查找
自身属性(own property)–>原型链上的属性–>直至null终止
1 | |
function Animal()
- 定义一个构造函数Animal
- 所用通过new Animal()创建的实例都会共享Animal.protype上的方法
Animal.prototype.eat = function()
- 在Animal的原型对象上添加eat方法
- 所有Animal实例(如dog)都能通过原型链访问它
const dog = new Animal();
- 创建Animal的一个实例dog
dog.__proto__===Animal.prototype–>true
dog.bark = function()
- 直接给dog实例添加一个自有属性bark
- 这个方法是dog实例有的不会被其他Animal实例共享
console.log(dog.eat());
- dog实例自身没有eat,于是沿原型链找
- dog–>
Animal.prototype(找到eat)–>执行 - 输出eating…
console.log(dog.bark());
- dog没有
toString,继续沿原型链查找 - dog–>
Animal.prototype(无toString)–>Object.prototype(找到toString) Object.prototype.toString()默认返回 [object Object]- 输
[object Object]
``console.log(dog.proto.proto);`
- dog.__proto__是
Animal.prototype Animal.prototype.__proto__是Object.prototypeObject.prototype.__proto__是null(原型链的终点)- 所以:
dog.__proto__.__proto__===Object.prototype–>true dog.__proto__.__proto__.__proto__===null–>true
说明所有 对象最终都继承自Object.prototype,它是整个原型链根节点
内置原型对象
| 原型对象 | 作用 |
|---|---|
Object.prototype |
所有对象的顶层原型,定义了如toString,hasOwnPropertu,valueOf等通用方法 |
Array.prototype |
提供数组操作方法:push,pop,map,filter,reduce等 |
Function.prototype |
函数的原型,包含call,apply,bind等方法 |
String.prototype |
字符串方法如split,replace,includes等 |
Number.prototype |
数值方法如toFixed,toExponential |
这些原型对象一旦被污染,将影响全局行为。比如修改Object.prototype.toString可以导致所有对象的字符串表示异常甚至泄敏感信息
Array.prototype被篡改
1 | |
这种污染可能破坏数据校验、权限控制、日志记录等关键流程
__proto__的特殊性与安全性隐患
__proto__是一个可枚举属性,这意味着它可以出现在for……in循环中,并且可以被直接赋值
1 | |
核心漏洞成因
由于__proto__是所有对象共有的内置属性,攻击者可通过构造含有__proto__字段的对象输入,使合并函数将其误认为普通属性进行递归处理,从而污染Object.prototype
原型继承的实现方式与典型模式
构造函数与原型模式
1 | |
定义构造函数
1 | |
这是一个构造函数,用于创造具有相同结构的“Person”对象
Person是一个普通函数,但约定首字母大写表示它是构造函数
使用new Person()调用时
- 会创建一个新对象{}
- 将这个对象的
__proto__指向Person.prototype - 执行函数体,其中this指向这个新对象
- 如果汉纳树没有显示返回对象,则自动返回这个新对象
this.name = name;将传入的参数name赋值给新对象name属性,这是实例自身的属性,不与其他共享
向原型添加方法
1 | |
将greet和isAdult两个方法添加到Person的原型对象上,供所有实例共享使用
创建实例并调用方法
1 | |
| 步骤 | alice 实例 |
bob 实例 |
|---|---|---|
| 创建 | new Person("Alice", 25) |
new Person("Bob", 16) |
| 自身属性 | name: "Alice", age: 25 |
name: "Bob", age: 16 |
| 原型链接 | alice.__proto__ === Person.prototype |
bob.__proto__ === Person.prototype |
调用方法并输出
1 | |
alice.greet()
JS引擎查找greet方法
- 先看alice自身有没有greet–>这里自身没有
- 沿原型链向上继续查找–>
alice.__proto__(即Person.prototype)–>找到greet函数
执行greet(),此时this=alice
返回:
Hello, I'm Alice, 25 years old.
bob.isAdult()
查找
isAdult- bob自身没有–>沿着原型链向上查找Person.prototype找到
执行时
this=bob,所以this.age=16判断16>=18–>false
原型链图解
1 | |
object.create()显式创建原型链
object.create()允许你显式指定对象的类型,是构建原型链的高级手段
1 | |
若传入恶意对象作为原型,可能导致原型污染
1 | |
ES6 类语法背后的原型机制
1 | |
class Car
定义一个Car类
constructor(brand, model)
构造函数,用于初始换属性brand和model
start()
实例方法,绑定在Car.prototype上,所有实例共享
static info()
静态方法,只属于Car.info本身,不能共享给其他类
const tesla = new Car("Tesla", "Model S");
将Car实例化成一个新的对象,内部属性为{ brand: “Tesla”, model: “Model S” }
console.log(tesla.start());
在tesla中调用公有实例方法start,访问了this.brand和this.model
等价转换
1 | |
显式原型赋值于隐式原型链查找的差异
| 行为 | 描述 | 示例 |
|---|---|---|
| 显示原型赋值 | 直接修改constructor.prototype |
Person.prototype.sayHi=()=>{} |
| 隐式原型链查找 | 通过 __proto__ |
obj.someMethod()会沿着原型链查找 |
1 | |
console.log(myDog.bark());
调用myDog.bark()时,JS首先在myDog对象自身上查找bark方法
由于myDog不存在bark方法,会通过原型链在Dog.prototype上找到该方法
Dog.prototype.bark = function() { return "Bark!"; };
直接修改Dog.prototype.bark时所有通过new Dog()创建的实例都会受到影响
⚠️JS的原型链继承是动态的
⚠️修改原型对象会影响所有已创建的和未创建的实例