JavaScript prototype pollution protection strategies for library developers

I’ve been looking into security issues in JavaScript libraries and found something concerning about prototype pollution attacks. It seems like many npm packages might be vulnerable to this type of attack.

Basic example of the problem:

var templateLib = require("some-template-engine");
var renderFunc = templateLib.compile("<div>Sample content {{=data.value}}</div>");
renderFunc({value: "test"});

How an attacker could exploit this:

var templateLib = require("some-template-engine");
// Malicious prototype pollution
Object.prototype.settings = {variable: "x,y,z,exec=console.log('hacked')"};
// Normal usage that gets compromised
var processor = templateLib.load({folder: "./templates"});
processor.render();

This got me thinking - can’t attackers mess with almost any JavaScript library that accepts configuration objects?

For instance, consider a web server setup:

var config = {
  staticFiles: 'allow',
  caching: true,
  fileTypes: ['js', 'css'],
  indexFile: true,
  cacheTime: '2h',
  autoRedirect: false,
  customHeaders: function (response, filepath, stats) {
    response.set('custom-header', new Date().getTime());
  }
};

server.use(staticHandler('assets', config));

What if someone sets Object.prototype.autoRedirect = true? This could cause unexpected behavior when the library processes the config object.

As someone who develops npm packages, what are the best practices to prevent these prototype pollution attacks while still allowing users to pass configuration options? I’m particularly interested in solutions that library authors can implement to protect their users.

Indeed, prototype pollution is a significant issue that developers must address to maintain the integrity of their libraries. One effective strategy is to utilize Object.create(null) for configuration objects, which prevents inheritance from Object.prototype. This allows for the merging of user configurations into a secure base object: const safeConfig = Object.assign(Object.create(null), defaultConfig, userConfig). Additionally, it’s crucial to be explicit about the required properties by avoid using for...in loops. Instead, you can rely on Object.hasOwnProperty() checks or directly destructure necessary properties. For libraries that manage sensitive information, incorporating schema validation with libraries like Joi or Yup is advisable, despite the added complexity and overhead it may introduce.

freeze your objects after setup. i use Object.freeze() on default configs so even if someone pollutes the prototype, the actual config stays untouched. also try destructuring with defaults like const {port = 3000, host = 'localhost'} = userConfig || {} - you’re pulling specific values instead of inheriting random stuff from the prototype chain.

Here’s another approach: use a whitelist-based property filter in your config handler. I’ve had good luck with a function that checks incoming config objects against predefined allowed properties before processing. Something like const sanitizeConfig = (userConfig, allowedKeys) => allowedKeys.reduce((acc, key) => userConfig.hasOwnProperty(key) ? {...acc, [key]: userConfig[key]} : acc, {}). Even if prototype pollution happens, your library only processes properties you’ve explicitly approved. You can also use the Map constructor for storing config data since it doesn’t inherit from Object.prototype. Yeah, it’s more upfront work defining your API surface, but you get stronger guarantees about what data your library actually consumes. Performance hit is minimal since you’re filtering once during initialization, not on every operation.