目录
前置知识
原型对象
prototype和__proto__的区别
原型链概念
原型链的继承
原型 链污染
原型链污染原理
javascript中可能会存在原型链污染的危险函数
原型链污染的实际应用
JavaScript中可以触发弹窗的函数
前置知识
原型对象
在JavaScript中,每个函数都有一个特殊的属性prototype
,它指向一个对象,这个对象就是所谓的原型对象。原型对象是一个普通的JavaScript对象,它包含着一些属性和方法,这些属性和方法可以被该函数的所有实例共享。
当使用new
操作符来创建一个实例对象时,实例对象会继承它所属的构造函数的原型对象中的属性和方法。实例对象可以通过原型链访问原型对象中的属性和方法。如果实例对象需要访问的属性或方法在自身上找不到,它就会沿着原型链向上查找,直到找到为止。
以下是一个简单的示例,演示了如何使用原型对象:
// 定义一个构造函数 function Person(name, age) { this.name = name; this.age = age; } // 在原型对象上定义一个方法 Person.prototype.sayHello = function() { console.log('Hello, my name is ' + this.name + ' and I am ' + this.age + ' years old.'); }; // 创建两个实例对象 var person1 = new Person('Alice', 25); var person2 = new Person('Bob', 30); // 调用实例对象上的方法 person1.sayHello(); // Hello, my name is Alice and I am 25 years old. person2.sayHello(); // Hello, my name is Bob and I am 30 years old. // 查看实例对象的原型 console.log(person1.__proto__ === Person.prototype); // true console.log(person2.__proto__ === Person.prototype); // true // 查看原型对象的构造函数 console.log(Person.prototype.constructor === Person); // true
prototype和__proto__的区别
在 JavaScript 中,每个对象都有一个 __proto__
属性,它指向该对象的原型。原型是一个对象,也可以有自己的原型,这样就形成了一个原型链。同时,每个函数也有一个 prototype
属性,它是一个对象,当该函数作为构造函数创建实例时,实例对象的 __proto__
属性会指向该构造函数的 prototype
属性,这样就可以实现属性和方法的继承。
下面是 prototype
和 __proto__
的区别:
-
prototype
属性是函数所独有的,而__proto__
属性是每个对象都有的。 -
prototype
属性指向一个对象,它是用来存储属性和方法,这些属性和方法可以被该函数的实例对象所继承。而__proto__
属性指向该对象的原型,它是用来实现对象之间的继承。
下面是一些示例代码,可以帮助理解 prototype
和 __proto__
:
// 定义一个构造函数 function Person(name, age) { this.name = name; this.age = age; } // 在 Person 的原型上定义一个方法 Person.prototype.sayHello = function() { console.log('Hello, my name is ' + this.name + '.'); } // 创建一个 Person 实例对象 var person = new Person('Alice', 20); // 输出实例对象的属性和方法 console.log(person.name); // "Alice" console.log(person.age); // 20 person.sayHello(); // "Hello, my name is Alice." // 输出 Person 和实例对象的 __proto__ 属性 console.log(Person.__proto__); // [Function] console.log(person.__proto__); // Person { sayHello: [Function] } // 输出 Person 和实例对象的 prototype 属性 console.log(Person.prototype); // Person { sayHello: [Function] } console.log(person.prototype); // undefined
从上面的示例代码可以看出,prototype
属性被用于在函数的原型上定义方法和属性,而 __proto__
属性被用于实现实例对象和构造函数之间的继承关系。
原型链概念
当创建一个 JavaScript 对象时,它都会有一个原型对象(也称为“proto”)。可以通过该对象来访问该对象的属性和方法。如果该对象本身没有定义某个属性或方法,那么 JavaScript 会在原型对象中查找,如果还没有找到,会继续查找原型对象的原型对象,直到找到 Object 的原型对象为止。
以下是一个简单的例子来演示原型链的概念:
// 定义一个构造函数 function Person(name, age) { this.name = name; this.age = age; } // 在构造函数的原型对象上定义一个方法 Person.prototype.sayHello = function() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); }; // 创建一个 Person 实例 const person1 = new Person('Alice', 25); // 调用该实例的 sayHello 方法 person1.sayHello(); // 输出:Hello, my name is Alice and I am 25 years old. // 查看该实例的原型对象 console.log(person1.__proto__); // 输出:{ sayHello: [Function] } // 查看该实例的原型对象的原型对象 console.log(person1.__proto__.__proto__); // 输出:{ constructor: [Function: Object], ... }
在上面的例子中,定义了一个 Person
构造函数,该构造函数有两个属性 name
和 age
,并在构造函数的原型对象上定义了一个方法 sayHello
。然后,创建了一个 person1
实例,并调用了该实例的 sayHello
方法。当调用 person1.sayHello()
时,JavaScript 在该实例对象中查找该方法,但是它发现该对象本身没有定义该方法,因此它会查找该对象的原型对象,即 Person.prototype
,在该原型对象中找到了该方法并执行。这就是原型链的概念。
原型链的继承
基于原型链的继承:
// 定义一个父类 function Animal(name) { this.name = name; } Animal.prototype.sayName = function() { console.log(this.name); }; // 定义一个子类 function Dog(name, breed) { this.breed = breed; Animal.call(this, name); // 调用父类的构造函数来初始化属性 } // 设置子类的原型继承自父类 Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; // 子类可以使用父类的方法和属性 var dog = new Dog('Tommy', 'Golden Retriever'); dog.sayName(); // "Tommy" console.log(dog.breed); // "Golden Retriever"
基于对象的继承:
// 定义一个父对象 var animal = { name: '', sayName: function() { console.log(this.name); } }; // 定义一个子对象 var dog = Object.create(animal); dog.breed = ''; // 子对象可以使用父对象的方法和属性 dog.name = 'Tommy'; dog.breed = 'Golden Retriever'; dog.sayName(); // "Tommy" console.log(dog.breed); // "Golden Retriever"
原型链污染
当在 JavaScript 中使用不可信的数据时,可能会导致原型链污染问题。以下是一个示例代码,演示了如何利用原型链污染来执行恶意代码:
// 从不受信任的数据创建一个 JavaScript 对象 const user = JSON.parse('{"__proto__": {"isAdmin": true}}'); // 恶意代码:修改所有对象的 toString 方法,以便执行任意的 JavaScript 代码 Object.prototype.toString = function() { if (this.isAdmin) { // 恶意代码:执行任意的 JavaScript 代码 console.log('恶意代码:您的计算机已被攻陷!'); // 恶意代码:发送您的敏感数据到黑客服务器 console.log('恶意代码:正在发送您的银行卡信息和密码到黑客服务器!'); } // 恶意代码:调用 toString 原始实现 return '[object Object]'; }; // 在不知情的情况下,调用 toString 方法 console.log(user.toString()); // 输出:恶意代码:您的计算机已被攻陷! 恶意代码:正在发送您的银行卡信息和密码到黑客服务器! [object Object]
在上面的代码中,从一个不可信的数据源(例如一个恶意网站)解析出一个 JavaScript 对象 user
,其中该对象的 __proto__
属性被设置为一个新的原型对象,该原型对象有一个 isAdmin
属性,值为 true
。接下来,在 Object.prototype
上修改了 toString
方法,以便执行任意的 JavaScript 代码。最后,调用 user.toString()
方法,JavaScript 沿着原型链查找该方法,并在 user
对象的原型对象中找到了它,导致恶意代码被执行。
因此,原型链污染是一种严重的安全问题,可能会导致代码执行任意的 JavaScript 代码。
原型链污染原理
在 JavaScript 中,每个对象都有一个原型对象,当访问一个对象的属性时,JavaScript 会沿着该对象的原型链向上查找,直到找到该属性或者到达原型链的顶端为止。原型链污染是指攻击者通过修改对象的原型链来实现对代码的攻击。
下面是一个简单的示例,演示了原型链污染的基本原理:
// 定义一个基础对象,包含一个 getName 方法
const baseObj = {
getName: function() {
return 'I am a base object';
}
};
// 定义一个恶意对象,通过原型链污染修改了 getName 方法
const maliciousObj = {
getName: function() {
return 'I am a malicious object';
}
};
maliciousObj.__proto__ = baseObj; // 修改原型对象为 baseObj
// 创建一个普通的对象,并访问 getName 方法
const normalObj = {};
console.log(normalObj.getName()); // 输出:Uncaught TypeError: normalObj.getName is not a function
// 修改 normalObj 对象的原型为 maliciousObj
Object.setPrototypeOf(normalObj, maliciousObj);
// 此时再次访问 getName 方法,输出被污染的内容
console.log(normalObj.getName()); // 输出:I am a malicious object
在上面的示例中,定义了一个基础对象 baseObj
,它包含一个 getName
方法,该方法返回一个固定的字符串。接下来,定义了一个恶意对象 maliciousObj
,该对象同样包含一个 getName
方法,但它返回的字符串不同,并且将 __proto__
属性设置为 base
Obj
对象,从而使 maliciousObj
对象的原型指向了 baseObj
。接下来,创建了一个普通的对象 normalObj
,该对象在原型链上并没有定义 getName
方法。然后,使用 Object.setPrototypeOf
方法将 normalObj
的原型设置为 maliciousObj
,从而在 normalObj
对象上成功实现了原型链污染。最后,调用 normalObj.getName()
方法时,JavaScript 沿着 normalObj
对象的原型链查找到了 maliciousObj
对象,并返回了被污染的 getName
方法的内容。
javascript中可能会存在原型链污染的危险函数
merge
函数:
当使用 merge
函数合并两个对象时,如果这两个对象都有相同的属性,那么 merge
函数会将目标对象中的属性值替换为源对象中的属性值。然而,如果这些属性的值是对象或者数组,那么 merge
函数并不会复制它们的值,而是将它们的引用复制给了目标对象。这样,如果修改目标对象中的这些属性,那么源对象中的这些属性也会被修改,进而污染了原型链。
下面是一个示例代码,它展示了如何使用 merge
函数修改全局作用域中的对象,从而污染了原型链:
// 定义一个全局变量 var globalObj = {}; // 在 globalObj 的原型上定义一个属性 Object.prototype.globalProp = 'global property'; // 合并一个带有属性的对象到全局变量中 _.merge(globalObj, { myProp: 'my property' }); // 修改全局变量中的属性 globalObj.globalProp = 'modified global property'; // 输出全局变量中的属性和原型链上的属性 console.log(globalObj.myProp); // "my property" console.log(globalObj.globalProp); // "modified global property" // 创建一个对象并继承自全局变量 var obj = Object.create(globalObj); // 输出继承自全局变量的对象的属性和原型链上的属性 console.log(obj.myProp); // "my property" console.log(obj.globalProp); // "modified global property"
从上面的示例代码可以看出,由于 merge
函数修改了全局变量中的属性,因此继承自全局变量的对象也受到了污染。
clone
函数:
在JavaScript中,对象可以被复制或克隆,有时候需要将一个对象的属性和方法复制到另一个对象中。通常会使用一些函数库提供的clone
函数来实现对象的复制,但是有些clone
函数实现方式可能会造成原型链污染的风险。
原型链污染的风险在于,攻击者可以通过污染原型对象,从而控制对象的行为并进行恶意操作。
下面是一个clone
函数的示例,它使用了Object.assign
函数来实现对象的复制:
function cloneObject(source) { return Object.assign({}, source); }
这个cloneObject
函数实现了对象的复制,它接受一个源对象source
作为参数,然后返回一个新的对象,新对象的属性和方法与源对象相同。但是这个cloneObject
函数存在原型链污染的风险,因为Object.assign
函数会复制源对象的原型对象中的属性和方法。
下面是一个利用这个cloneObject
函数进行原型链污染的示例代码:
// 定义一个原型对象 var myPrototype = { sayHello: function() { console.log('Hello!'); } }; // 创建一个空对象 var myObject = {}; // 使用cloneObject函数复制myPrototype对象到myObject中 cloneObject(myObject, myPrototype); // 调用myObject上的sayHello方法 myObject.sayHello(); // Hello!
在这个示例代码中,定义了一个原型对象myPrototype
,它包含一个方法sayHello
。然后创建一个空对象myObject
,并使用cloneObject
函数将myPrototype
对象复制到myObject
中。由于cloneObject
函数使用了Object.assign
函数,因此myObject
的原型对象被设置为了myPrototype
,myObject
现在可以访问原型对象中的方法sayHello
。因此,调用myObject.sayHello()
时,就会执行myPrototype
中的sayHello
方法。
Object.assign
函数:
Object.assign
是一个常用的函数,用于将多个源对象的属性和方法合并到目标对象中。然而,Object.assign
也可能会导致原型链污染的问题。
Object.assign
的原理是遍历源对象的所有可枚举属性,然后将它们赋值给目标对象。如果源对象的属性是一个引用类型(如对象、数组等),则会将引用复制到目标对象中,这可能会造成原型链污染的风险。
// 定义一个全局变量 var globalObj = {}; // 在 globalObj 的原型上定义一个属性 Object.prototype.globalProp = 'global property'; // 使用 Object.assign 合并一个带有属性的对象到全局变量中 Object.assign(globalObj, { myProp: 'my property' }); // 修改全局变量中的属性 globalObj.globalProp = 'modified global property'; // 输出全局变量中的属性和原型链上的属性 console.log(globalObj.myProp); // "my property" console.log(globalObj.globalProp); // "modified global property" // 创建一个对象并继承自全局变量 var obj = Object.create(globalObj); // 输出继承自全局变量的对象的属性和原型链上的属性 console.log(obj.myProp); // "my property" console.log(obj.globalProp); // "modified global property"·
下面是一个使用Object.assign
函数导致原型链污染的示例代码:
// 定义一个原型对象 var myPrototype = { sayHello: function() { console.log('Hello!'); } }; // 创建一个空对象 var myObject = {}; // 使用Object.assign函数将myPrototype对象合并到myObject中 Object.assign(myObject, myPrototype); // 调用myObject上的sayHello方法 myObject.sayHello(); // Hello!
在这个示例代码中,定义了一个原型对象myPrototype
,它包含一个方法sayHello
。然后创建一个空对象myObject
,并使用Object.assign
函数将myPrototype
对象合并到myObject
中。由于Object.assign
函数会将myPrototype
对象中的属性和方法复制到myObject
中,因此myObject
的原型对象被设置为了myPrototype
,myObject
现在可以访问原型对象中的方法sayHello
。因此,调用myObject.sayHello()
时,就会执行myPrototype
中的sayHello
方法。
Object.create
函数:
Object.create
是一个用于创建新对象的函数,它可以使用现有对象作为新对象的原型。
// 定义一个全局变量 var globalObj = {}; // 在 globalObj 的原型上定义一个属性 Object.prototype.globalProp = 'global property'; // 使用 Object.create 创建一个带有属性的对象并继承自全局变量 var obj = Object.create(globalObj, { myProp: { value: 'my property' } }); // 修改全局变量中的属性 globalObj.globalProp = 'modified global property'; // 输出继承自全局变量的对象的属性和原型链上的属性 console.log(obj.myProp); // "my property" console.log(obj.globalProp); // "modified global property"
如果原型对象存在恶意代码,那么新对象也会受到影响,从而导致原型链污染的问题。
下面是一个使用Object.create
函数导致原型链污染的示例代码:
// 定义一个原型对象 var myPrototype = { sayHello: function() { console.log('Hello!'); } }; // 使用Object.create函数创建一个新对象,并将myPrototype作为它的原型 var myObject = Object.create(myPrototype); // 调用myObject上的sayHello方法 myObject.sayHello(); // Hello!
在这个示例代码中,定义了一个原型对象myPrototype
,它包含一个方法sayHello
。然后使用Object.create
函数创建了一个新对象myObject
,并将myPrototype
作为它的原型。由于myObject
的原型对象被设置为了myPrototype
,myObject
现在可以访问原型对象中的方法sayHello
。因此,调用myObject.sayHello()
时,就会执行myPrototype
中的sayHello
方法。
eval
函数:
eval
函数是一个全局函数,它将传入的字符串作为 JavaScript 代码进行解析和执行。
// 定义一个全局变量 var globalVar = 'global variable'; // 使用 eval 函数创建一个函数并在其中访问全局变量 eval('function myFunc() { console.log(globalVar); }'); // 修改全局变量的值 globalVar = 'modified global variable'; // 调用刚才创建的函数 myFunc(); // "modified global variable"
由于eval
函数具有执行任意代码的能力,因此它也可能导致原型链污染的问题。
在eval
函数中,如果执行的代码中包含了对__proto__
属性的操作,那么就会导致原型链污染。例如,下面的示例代码演示了如何使用eval
函数向一个对象的原型对象中添加一个属性:
var myObject = {}; var code = "this.__proto__.myProperty = 'Hello World';"; // 向原型对象添加属性 eval(code); // 执行代码 console.log(myObject.myProperty); // Hello World
在这个示例代码中,定义了一个空对象myObject
,然后使用字符串形式的代码向它的原型对象中添加了一个属性myProperty
。将这个字符串代码传递给eval
函数,并执行它。由于eval
函数会将字符串代码作为 JavaScript 代码进行解析和执行,因此会向myObject
的原型对象中添加一个属性myProperty
。最后,输出myObject.myProperty
的值,可以看到它已经被设置为了'Hello World'
。
原型链污染的实际应用
示例代码:
// 定义一个常量对象 const MY_CONST = { name: "My Const" }; // 定义一个 merge 函数,用于合并对象 function merge(target, source) { for (let key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } } // 使用 merge 函数合并对象 merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}')); // 检查 MY_CONST 是否被污染 console.log(MY_CONST.isAdmin); // true
在这个示例中,首先定义了一个常量对象MY_CONST
,然后定义了一个merge
函数,用于合并两个对象。接着,使用JSON.parse
函数解析了一个包含__proto__
属性的 JSON 字符串,并将它作为参数传递给merge
函数。由于merge
函数中没有对参数进行类型检查或者属性白名单过滤,因此这个函数可能会合并任意对象的属性,包括它们的原型对象中的属性。
在这个示例中,由于恶意 JSON 字符串中包含了__proto__
属性,因此它会将一个具有isAdmin
属性的对象添加到MY_CONST
对象的原型对象中。这样,就可以通过MY_CONST.isAdmin
属性访问到这个恶意对象中的属性值了,这就是原型链污染的问题。
JavaScript中可以触发弹窗的函数
-
alert()
函数可以在浏览器中弹出一个警告框,用于向用户显示一条消息。示例代码如下:alert('Hello, world!');
-
confirm()
函数可以在浏览器中弹出一个确认框,用于向用户显示一条消息并询问用户是否继续操作。示例代码如下:if (confirm('Are you sure you want to delete this item?')) { // 用户点击了确认按钮,执行删除操作 } else { // 用户点击了取消按钮,不执行删除操作 }
-
prompt()
函数可以在浏览器中弹出一个对话框,用于向用户显示一条消息并询问用户输入内容。示例代码如下:let name = prompt('What is your name?'); if (name) { alert('Hello, ' + name + '!'); } else { alert('Please enter your name.'); }