MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript类和构造函数的安全设计

2021-01-132.9k 阅读

JavaScript 类和构造函数基础回顾

在深入探讨 JavaScript 类和构造函数的安全设计之前,我们先来回顾一下它们的基础知识。

构造函数

在 JavaScript 早期,并没有类的概念,开发者使用构造函数来创建对象实例。构造函数本质上就是一个普通函数,不过它遵循一些特定的约定。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    };
}

let person1 = new Person('Alice', 30);
person1.sayHello(); 

在上述代码中,Person 函数就是一个构造函数。使用 new 关键字调用构造函数时,会发生以下几件事:

  1. 创建一个新的空对象。
  2. 这个新对象的 [[Prototype]] 被设置为构造函数的 prototype 属性。
  3. 构造函数内部的 this 指向这个新对象。
  4. 执行构造函数的代码,为新对象添加属性和方法。
  5. 如果构造函数没有显式返回一个对象,则返回这个新创建的对象。

JavaScript 类

ES6 引入了类的语法糖,使得 JavaScript 的面向对象编程更加直观和易于理解。类本质上是基于构造函数和原型链的语法封装。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    sayHello() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}

let person2 = new Person('Bob', 25);
person2.sayHello(); 

这里的 class Person 定义了一个类,constructor 方法是类的构造函数,和传统构造函数类似。类的方法(如 sayHello)定义在类的原型上,这和通过构造函数给原型添加方法是等价的。

类和构造函数安全设计的重要性

随着 JavaScript 在前端和后端(如 Node.js)应用中的广泛使用,确保类和构造函数的安全设计变得至关重要。

防止属性污染

如果构造函数或类没有进行恰当的设计,外部代码可能会意外或恶意地修改对象的内部状态,导致程序出现不可预期的行为。

function BadlyDesignedObject() {
    this.publicProp = 'public value';
    this.internalProp = 'internal value';
}

let badObj = new BadlyDesignedObject();
// 外部代码可以随意修改内部属性
badObj.internalProp = 'tampered value';

在上述例子中,BadlyDesignedObject 没有对内部属性进行保护,外部代码可以轻易修改 internalProp。这可能会导致依赖该内部状态的其他功能出现错误。

避免原型链污染

原型链污染是一种严重的安全漏洞,攻击者可以通过修改原型对象来影响所有基于该原型创建的对象。

function VulnerableObject() {}

VulnerableObject.prototype.commonMethod = function() {
    return 'original behavior';
};

let vulnerableObj = new VulnerableObject();

// 恶意代码
Object.prototype.newMaliciousMethod = function() {
    return 'malicious behavior';
};

// 所有对象现在都有了这个恶意方法
console.log(vulnerableObj.newMaliciousMethod()); 

在这个例子中,恶意代码通过给 Object.prototype 添加方法,影响了所有对象,包括 VulnerableObject 的实例。

保护构造函数调用

确保构造函数只能通过 new 关键字调用是很重要的。如果构造函数被当作普通函数调用,this 的指向会发生改变,可能导致对象状态错误。

function ConstructorWithoutProtection(name) {
    this.name = name;
}

// 错误调用,没有使用 new 关键字
let wrongCall = ConstructorWithoutProtection('Wrong Call');
console.log(wrongCall.name); // 这里会报错,因为 this 指向全局对象(浏览器环境中是 window)

类和构造函数的安全设计实践

保护内部状态

  1. 使用 WeakMap 进行属性封装 WeakMap 是一种特殊的键值对集合,其中的键必须是对象。它可以用于实现私有属性,因为 WeakMap 的键值对是无法直接访问的。
const privateData = new WeakMap();

class SecureObject {
    constructor() {
        let privateValue = 'private data';
        privateData.set(this, { privateValue });
    }

    getPrivateValue() {
        return privateData.get(this).privateValue;
    }
}

let secureObj = new SecureObject();
// 外部无法直接访问 privateValue
// 只能通过 getPrivateValue 方法访问
console.log(secureObj.getPrivateValue()); 

在上述代码中,privateData 是一个 WeakMap,通过将 this 作为键,存储了对象的私有数据。外部代码无法直接访问 WeakMap 中的数据,从而实现了属性的封装。

  1. 使用 # 前缀(ES2022 私有字段) ES2022 引入了私有字段,通过在字段名前添加 # 前缀来表示。
class SecureObjectWithHash {
    #privateValue = 'private data';

    getPrivateValue() {
        return this.#privateValue;
    }
}

let secureObjWithHash = new SecureObjectWithHash();
// 外部无法直接访问 #privateValue
console.log(secureObjWithHash.getPrivateValue()); 

防止原型链污染

  1. 冻结原型 可以使用 Object.freeze 方法冻结对象的原型,防止在运行时对原型进行修改。
function SecureConstructor() {}

SecureConstructor.prototype.secureMethod = function() {
    return'secure behavior';
};

// 冻结原型
Object.freeze(SecureConstructor.prototype);

let secureInstance = new SecureConstructor();
// 尝试添加新方法会失败,不会抛出错误但也不会生效
secureInstance.__proto__.newMethod = function() {
    return 'new method';
};
console.log(secureInstance.newMethod()); // undefined
  1. 使用 Object.seal Object.seal 方法可以阻止新属性的添加,并将所有现有属性标记为不可配置,但属性值仍然可以修改。
function AnotherSecureConstructor() {}

AnotherSecureConstructor.prototype.anotherSecureMethod = function() {
    return 'another secure behavior';
};

// 密封原型
Object.seal(AnotherSecureConstructor.prototype);

let anotherSecureInstance = new AnotherSecureConstructor();
// 尝试添加新方法会失败
anotherSecureInstance.__proto__.newMethod = function() {
    return 'new method';
};
console.log(anotherSecureInstance.newMethod()); // undefined

// 现有方法属性值仍可修改
AnotherSecureConstructor.prototype.anotherSecureMethod = function() {
    return'modified behavior';
};
console.log(anotherSecureInstance.anotherSecureMethod()); 

确保构造函数正确调用

  1. 在构造函数内部检查 this 构造函数内部可以通过检查 this 的类型来确保是通过 new 关键字调用的。
function CorrectCallConstructor(name) {
    if (!(this instanceof CorrectCallConstructor)) {
        throw new Error('Constructor must be called with new');
    }
    this.name = name;
}

// 正确调用
let correctCall = new CorrectCallConstructor('Correct Name');
// 错误调用会抛出错误
// let wrongCall = CorrectCallConstructor('Wrong Name'); 
  1. 使用辅助函数确保正确调用 可以创建一个辅助函数来确保构造函数的正确调用。
function ensureNew(constructor) {
    return function(...args) {
        if (!(this instanceof constructor)) {
            return new constructor(...args);
        }
        constructor.apply(this, args);
    };
}

function EnsuredConstructor(name) {
    this.name = name;
}

let EnsuredConstructorWithCheck = ensureNew(EnsuredConstructor);

// 可以正确调用,即使不使用 new 关键字
let ensuredInstance = EnsuredConstructorWithCheck('Ensured Name');

继承中的安全设计

安全的继承实现

在 JavaScript 中,类的继承通过 extends 关键字实现。在继承过程中,也需要注意安全设计。

  1. 调用 super() 在子类的构造函数中,必须调用 super() 来初始化父类的构造函数,否则 this 在子类构造函数中无法正确使用。
class Animal {
    constructor(name) {
        this.name = name;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        // 必须先调用 super()
        super(name);
        this.breed = breed;
    }
}

let myDog = new Dog('Buddy', 'Golden Retriever');
  1. 防止重写不可重写的方法 如果父类有一些方法是不希望被子类重写的,可以在父类中进行一些保护措施。
class ParentClass {
    constructor() {
        // 创建一个不可配置的方法
        Object.defineProperty(this, 'protectedMethod', {
            value: function() {
                return 'protected behavior';
            },
            writable: false,
            configurable: false
        });
    }
}

class ChildClass extends ParentClass {
    constructor() {
        super();
        // 尝试重写 protectedMethod 不会生效
        this.protectedMethod = function() {
            return 'new behavior';
        };
    }
}

let childInstance = new ChildClass();
console.log(childInstance.protectedMethod()); 

避免继承中的原型链污染

和普通对象一样,继承链中的原型也可能受到污染。

  1. 冻结继承链上的原型 可以在定义子类后,对其原型进行冻结。
class BaseClass {
    baseMethod() {
        return 'base behavior';
    }
}

class SubClass extends BaseClass {
    subMethod() {
        return'sub behavior';
    }
}

// 冻结子类原型
Object.freeze(SubClass.prototype);

let subInstance = new SubClass();
// 尝试添加新方法到子类原型会失败
subInstance.__proto__.newMethod = function() {
    return 'new method';
};
console.log(subInstance.newMethod()); // undefined

应用场景中的安全设计考虑

前端应用中的类和构造函数安全

  1. 防止 XSS 攻击相关的安全设计 在前端,用户输入可能会导致跨站脚本(XSS)攻击。当类或构造函数处理用户输入时,需要进行严格的输入验证和转义。
class UserInputProcessor {
    constructor(input) {
        // 假设这是一个简单的输入验证,实际应用中应更复杂
        if (!input.match(/^[a-zA-Z0-9 ]+$/)) {
            throw new Error('Invalid input');
        }
        this.sanitizedInput = input.replace(/[<>]/g, function(match) {
            return { '<': '&lt;', '>': '&gt;' }[match];
        });
    }

    displayInput() {
        let div = document.createElement('div');
        div.innerHTML = this.sanitizedInput;
        document.body.appendChild(div);
    }
}

let userInput = 'test <script>alert("XSS")</script>';
try {
    let processor = new UserInputProcessor(userInput);
    processor.displayInput();
} catch (error) {
    console.error(error.message);
}

在上述代码中,UserInputProcessor 类对用户输入进行了验证和转义,防止恶意脚本被注入到 DOM 中。

  1. 数据绑定和模板渲染中的安全 在使用数据绑定和模板渲染库时,如 React 或 Vue.js,类和构造函数处理的数据也需要进行安全处理。
class DataForTemplate {
    constructor(data) {
        // 假设 data 是一个对象,对其中的字符串值进行转义
        for (let key in data) {
            if (typeof data[key] ==='string') {
                data[key] = data[key].replace(/[&<>"']/g, function(match) {
                    return {
                        '&': '&amp;',
                        '<': '&lt;',
                        '>': '&gt;',
                        '"': '&quot;',
                        "'": '&#39;'
                    }[match];
                });
            }
        }
        this.safeData = data;
    }
}

let templateData = { message: 'test <script>alert("XSS")</script>' };
let safeData = new DataForTemplate(templateData).safeData;
// 在模板中使用 safeData 可以防止 XSS 攻击

后端 Node.js 应用中的类和构造函数安全

  1. 防止注入攻击 在 Node.js 中,处理数据库查询或命令行参数时,类和构造函数需要防止注入攻击。
const mysql = require('mysql');

class DatabaseQuery {
    constructor(query, values) {
        // 假设 values 是一个数组,对查询进行参数化处理
        this.connection = mysql.createConnection({
            host: 'localhost',
            user: 'root',
            password: 'password',
            database: 'test'
        });
        this.query = this.connection.format(query, values);
    }

    execute() {
        return new Promise((resolve, reject) => {
            this.connection.query(this.query, (error, results, fields) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(results);
                }
                this.connection.end();
            });
        });
    }
}

let query = 'SELECT * FROM users WHERE username =? AND password =?';
let values = ['admin', 'password123'];
let databaseQuery = new DatabaseQuery(query, values);
databaseQuery.execute().then(results => {
    console.log(results);
}).catch(error => {
    console.error(error);
});

在上述代码中,DatabaseQuery 类对数据库查询进行了参数化处理,防止 SQL 注入攻击。

  1. 进程间通信安全 在 Node.js 中,如果使用进程间通信(IPC),类和构造函数传递的数据需要进行安全验证。
const { fork } = require('child_process');

class IPCDataSender {
    constructor(data) {
        // 假设 data 是一个对象,验证其属性
        if (!data.hasOwnProperty('message') || typeof data.message!=='string') {
            throw new Error('Invalid data');
        }
        this.safeData = data;
    }

    sendToChild() {
        let child = fork('child.js');
        child.send(this.safeData);
        child.on('message', (message) => {
            console.log('Received from child:', message);
        });
        child.on('close', () => {
            console.log('Child process closed');
        });
    }
}

let dataToSend = { message: 'Hello from parent' };
let sender = new IPCDataSender(dataToSend);
sender.sendToChild();

在这个例子中,IPCDataSender 类对要发送到子进程的数据进行了验证,确保数据的安全性。

总结安全设计要点及最佳实践

  1. 属性封装:使用 WeakMap 或 ES2022 的 # 前缀来保护对象的内部状态,防止外部直接访问和修改。
  2. 原型保护:通过 Object.freezeObject.seal 来防止原型链被污染,确保对象的行为不会被意外改变。
  3. 构造函数调用保护:在构造函数内部检查 this 或者使用辅助函数来确保构造函数只能通过 new 关键字调用。
  4. 继承安全:在子类构造函数中正确调用 super(),并对继承链上的原型进行必要的保护,防止重写不应重写的方法。
  5. 应用场景特定安全:在前端要防止 XSS 攻击,在后端要防止注入攻击等,对输入数据进行严格的验证和处理。

通过遵循这些安全设计要点和最佳实践,可以有效地提高 JavaScript 类和构造函数的安全性,减少潜在的安全漏洞。无论是开发小型的前端应用还是大型的后端服务,安全始终是至关重要的。在实际开发中,应根据具体的应用场景和需求,综合运用这些安全设计方法,确保代码的健壮性和安全性。