CVE-2020-28471: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: This affects the package properties-reader before 2.2.0.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 9.8
Published: 2022-07-25T14:15Z
Repository: https://github.com/steveukx/properties
Language: JavaScript
Files changed: src/properties-reader.js, test/fix-prototype-pollution.spec.js
Commit date: 2020-12-30 08:54:03 +0200
Commit message: Test case covering the use of `__proto__` as a section name
Closes #40

Diff: diff --git a/src/properties-reader.js b/src/properties-reader.js
index 1db639d..08ec72d 100644
--- a/src/properties-reader.js
+++ b/src/properties-reader.js
@@ -1,9 +1,11 @@
 const {readFileSync, statSync} = require('fs');
 const propertyAppender = require('./property-appender').propertyAppender;
 const propertyWriter = require('./property-writer').propertyWriter;
 
+const has = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
+
 const SECTION = Symbol('SECTION');
 
 function PropertiesReader (sourceFile, encoding, options = {}) {
    this._encoding = typeof encoding === 'string' && encoding || 'utf-8';
    this._properties = {};
@@ -211,11 +213,16 @@ PropertiesReader.prototype.set = function (key, value) {
    while (expanded.length > 1) {
       var step = expanded.shift();
       if (expanded.length >= 1 && typeof source[step] === 'string') {
          source[step] = {'': source[step]};
       }
-      source = (source[step] = source[step] || {});
+
+      if (!has(source, step)) {
+         Object.defineProperty(source, step, { value: Object.create(null) });
+      }
+
+      source = source[step]
    }
 
    if (typeof parsedValue === 'string' && typeof  source[expanded[0]] === 'object') {
       source[expanded[0]][''] = parsedValue;
    }
diff --git a/test/fix-prototype-pollution.spec.js b/test/fix-prototype-pollution.spec.js
new file mode 100644
index 0000000..19f0e21
--- /dev/null
+++ b/test/fix-prototype-pollution.spec.js
@@ -0,0 +1,28 @@
+const {createTestContext} = require('./__fixtues__/create-test-context');
+
+const propertiesReader = require('../');
+
+describe('prototype-pollution', () => {
+   let context;
+
+   beforeEach(async () => {
+      context = await createTestContext();
+   });
+
+   it('does not pollute global Object.prototype', async () => {
+      const file = `
+         [__proto__]
+            polluted = polluted
+            parsed = true
+      `;
+      const props = propertiesReader(await context.file('props.ini', file));
+
+      expect(({}).polluted).toBeUndefined();
+      expect(props.path().__proto__.polluted).toBe('polluted');
+      expect(props.getRaw('__proto__.polluted')).toBe('polluted');
+      expect(props.get('__proto__.polluted')).toBe('polluted');
+      expect(props.getRaw('__proto__.parsed')).toBe('true');
+      expect(props.get('__proto__.parsed')).toBe(true);
+   });
+
+});

Vulnerable code:       source = (source[step] = source[step] || {});
Fixed code: const has = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);


      if (!has(source, step)) {
         Object.defineProperty(source, step, { value: Object.create(null) });
      }

      source = source[step]
const {createTestContext} = require('./__fixtues__/create-test-context');

const propertiesReader = require('../');

describe('prototype-pollution', () => {
   let context;

   beforeEach(async () => {
      context = await createTestContext();
   });

   it('does not pollute global Object.prototype', async () => {
      const file = `
         [__proto__]
            polluted = polluted
            parsed = true
      `;
      const props = propertiesReader(await context.file('props.ini', file));

      expect(({}).polluted).toBeUndefined();
      expect(props.path().__proto__.polluted).toBe('polluted');
      expect(props.getRaw('__proto__.polluted')).toBe('polluted');
      expect(props.get('__proto__.polluted')).toBe('polluted');
      expect(props.getRaw('__proto__.parsed')).toBe('true');
      expect(props.get('__proto__.parsed')).toBe(true);
   });

});