CVE-2020-28461: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: This affects the package js-ini before 1.3.0. If an attacker submits a malicious INI file to an application that parses it with parse , they will pollute the prototype on the application. This can be exploited further depending on the context.
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/Sdju/js-ini
Language: TypeScript
Files changed: package-lock.json, package.json, src/errors/errors-symbol.ts, src/errors/index.ts, src/errors/parsing-error.ts, src/errors/proto-error.ts, src/index.ts, src/interfaces/custom-typing.ts, src/interfaces/ini-object.ts, src/parse.ts, src/proto.ts, test/tests.ts
Commit date: 2020-12-22 20:54:05 +0500
Commit message: refactoring and new functional

Diff: diff --git a/package-lock.json b/package-lock.json
index f9e02b7..46f3cb6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,8 +1,8 @@
 {
   "name": "js-ini",
-  "version": "1.1.5",
+  "version": "1.2.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
     "@babel/code-frame": {
       "version": "7.0.0-beta.42",
@@ -8360,13 +8360,13 @@
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
       "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
       "dev": true
     },
     "typescript": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
-      "integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
+      "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
       "dev": true
     },
     "uglify-js": {
       "version": "2.8.29",
       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
diff --git a/package.json b/package.json
index 3fd382e..c50ce34 100644
--- a/package.json
+++ b/package.json
@@ -36,11 +36,11 @@
     "eslint-config-airbnb-typescript": "^7.2.0",
     "eslint-plugin-import": "^2.20.2",
     "husky": "^4.2.3",
     "jest": "^22.4.3",
     "ts-jest": "^22.4.2",
-    "typescript": "3.4.3"
+    "typescript": "^4.1.3"
   },
   "jest": {
     "globals": {
       "ts-jest": {
         "tsConfigFile": "./tsconfig.json"
@@ -48,11 +48,11 @@
     },
     "transform": {
       "^.+\\.(ts|tsx)$": "ts-jest"
     },
     "testMatch": [
-      "**/test/*.+(ts|tsx|js)"
+      "**/test/**/*.+(ts|tsx|js)"
     ],
     "moduleFileExtensions": [
       "ts",
       "tsx",
       "js",
diff --git a/src/errors/errors-symbol.ts b/src/errors/errors-symbol.ts
new file mode 100644
index 0000000..4473d83
--- /dev/null
+++ b/src/errors/errors-symbol.ts
@@ -0,0 +1 @@
+export const $Errors: unique symbol = Symbol('Errors of parsing');
diff --git a/src/errors/index.ts b/src/errors/index.ts
new file mode 100644
index 0000000..37313ac
--- /dev/null
+++ b/src/errors/index.ts
@@ -0,0 +1,3 @@
+export * from './parsing-error';
+export * from './proto-error';
+export * from './errors-symbol';
diff --git a/src/errors.ts b/src/errors/parsing-error.ts
similarity index 60%
rename from src/errors.ts
rename to src/errors/parsing-error.ts
index 1893136..9eeb310 100644
--- a/src/errors.ts
+++ b/src/errors/parsing-error.ts
@@ -1,10 +1,8 @@
-export const $Errors: unique symbol = Symbol('Errors of parsing');
-
 export class ParsingError extends Error {
   constructor(line: string, lineNumber: number) {
-    super(`Unsupported type of line: [${lineNumber}]"${line}"`);
+    super(`Unsupported type of line: [${lineNumber}] "${line}"`);
     this.line = line;
     this.lineNumber = lineNumber;
   }
 
   public line: string;
diff --git a/src/errors/proto-error.ts b/src/errors/proto-error.ts
new file mode 100644
index 0000000..64449bf
--- /dev/null
+++ b/src/errors/proto-error.ts
@@ -0,0 +1,8 @@
+export class ProtoError extends Error {
+  constructor(lineNumber: number) {
+    super(`Unsupported section name "__proto__": [${lineNumber}]"`);
+    this.lineNumber = lineNumber;
+  }
+
+  public lineNumber: number;
+}
diff --git a/src/index.ts b/src/index.ts
index 6c62e4c..fb782cd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
 export * from './errors';
 export * from './parse';
 export * from './stringify';
+export * from './proto';
diff --git a/src/interfaces/custom-typing.ts b/src/interfaces/custom-typing.ts
index a966683..b366545 100644
--- a/src/interfaces/custom-typing.ts
+++ b/src/interfaces/custom-typing.ts
@@ -1,3 +1,3 @@
 export interface ICustomTyping {
-  (val: string, section: string, key: string): any
+  (val: string, section: string | symbol, key: string): any
 }
diff --git a/src/interfaces/ini-object.ts b/src/interfaces/ini-object.ts
index 81990ab..ec87781 100644
--- a/src/interfaces/ini-object.ts
+++ b/src/interfaces/ini-object.ts
@@ -1,6 +1,8 @@
 import { $Errors, ParsingError } from '../errors';
 import { IIniObjectSection } from './ini-object-section';
+import { $Proto } from '../proto';
 
 export interface IIniObject extends IIniObjectSection {
   [$Errors]?: ParsingError[];
+  [$Proto]?: IIniObjectSection;
 }
diff --git a/src/parse.ts b/src/parse.ts
index 309af4c..4428d79 100644
--- a/src/parse.ts
+++ b/src/parse.ts
@@ -1,18 +1,24 @@
-import { $Errors, ParsingError } from './errors';
+import {
+  $Errors,
+  ParsingError,
+  ProtoError,
+} from './errors';
 import { IIniObject } from './interfaces/ini-object';
 import { IniValue } from './types/ini-value';
 import { IIniObjectSection } from './interfaces/ini-object-section';
 import { autoType } from './helpers/auto-type';
 import { ICustomTyping } from './interfaces/custom-typing';
+import { $Proto } from './proto';
 
 export interface IParseConfig {
   comment?: string;
   delimiter?: string;
   nothrow?: boolean;
   autoTyping?: boolean | ICustomTyping;
   dataSections?: string[];
+  protoSymbol?: boolean;
 }
 
 const sectionNameRegex = /\[(.*)]$/;
 
 export function parse(data: string, params?: IParseConfig): IIniObject {
@@ -20,10 +26,11 @@ export function parse(data: string, params?: IParseConfig): IIniObject {
     delimiter = '=',
     comment = ';',
     nothrow = false,
     autoTyping = true,
     dataSections = [],
+    protoSymbol = false,
   } = { ...params };
   let typeParser: ICustomTyping;
   if (typeof autoTyping === 'function') {
     typeParser = autoTyping;
   } else {
@@ -43,13 +50,20 @@ export function parse(data: string, params?: IParseConfig): IIniObject {
       continue;
     } else if (line[0] === '[') {
       const match = line.match(sectionNameRegex);
       if (match !== null) {
         currentSection = match[1].trim();
+        if (currentSection === '__proto__') {
+          if (protoSymbol) {
+            currentSection = <string><any> $Proto;
+          } else {
+            throw new ProtoError(lineNumber);
+          }
+        }
         isDataSection = dataSections.includes(currentSection);
         if (!(currentSection in result)) {
-          result[currentSection] = (isDataSection) ? [] : {};
+          result[currentSection] = (isDataSection) ? [] : Object.create(null);
         }
         continue;
       }
     } else if (isDataSection) {
       (<IniValue[]>result[currentSection]).push(rawLine);
diff --git a/src/proto.ts b/src/proto.ts
new file mode 100644
index 0000000..e3f18cb
--- /dev/null
+++ b/src/proto.ts
@@ -0,0 +1 @@
+export const $Proto: unique symbol = Symbol('__proto__');
diff --git a/test/tests.ts b/test/tests.ts
index b40d815..1b7ca53 100644
--- a/test/tests.ts
+++ b/test/tests.ts
@@ -1,29 +1,37 @@
-import { parse, stringify } from '../src';
+import {
+  parse,
+  stringify,
+  $Proto,
+  $Errors,
+  ParsingError,
+} from '../src';
 
 const ini1 = `v1 = 2
 v-2=true
 v 3 = string
 [smbd]
 v1=5
-v2 = what 
+v2 = what
 ;comment
 v5 = who is who = who
 
 [test scope with spaces]
 mgm*1  = 2.5`;
+
 const ini2 = `v1 : 2
 v-2:true
 v 3 : string
 [smbd]
 v1:5
-v2 : what 
+v2 : what
 #comment
 v5 : who is who = who
 
 [test scope with spaces]
 mgm*1  : 2.5`;
+
 const ini3 = `v1=2
 v-2=true
 v 3=string
 
 [smbd]
@@ -31,19 +39,21 @@ v1=5
 v2=what
 v5=who is who = who
 
 [test scope with spaces]
 mgm*1=2.5`;
+
 const ini4 = `v1: 2
 v-2: true
 v 3: string
 [smbd]
 v1: 5
 v2: what
 v5: who is who = who
 [test scope with spaces]
 mgm*1: 2.5`;
+
 const ini5 = `v1: 2
 v-2: true
 v 3: string
 [smbd]
 v5: who is who = who
@@ -51,10 +61,28 @@ v2: what
 v1: 5
 [test scope with data]
 b1c,wdwd,15:68
 wx/w':wwdlw,:d,wld
 efkeofk`;
+
+const ini6 = `
+[ __proto__  ]
+polluted = "polluted"`;
+
+const ini7 = `
+[scope with trash]
+ok = value
+trash
+
+[scope with only trash]
+only trash
+
+[empty scope]
+[normal scope]
+ok = value
+`;
+
 const v1 = {
   v1: 2,
   'v-2': true,
   'v 3': 'string',
   smbd: {
@@ -151,5 +179,38 @@ test('ini stringify', () => {
   expect(parse(stringify(v3), {
     dataSections: ['test scope with data'],
     autoTyping: false,
   })).toEqual(v3);
 });
+
+test('ini parsing: proto', () => {
+  expect(() => parse(ini6))
+    .toThrow('Unsupported section name "__proto__": [2]"');
+
+  expect(parse(ini6, { protoSymbol: true }))
+    .toEqual({
+      [$Proto]: {
+        polluted: '"polluted"',
+      },
+    });
+});
+
+test('ini parsing: errors', () => {
+  expect(() => parse(ini7))
+    .toThrow('Unsupported type of line: [4] "trash"');
+
+  expect(parse(ini7, { nothrow: true }))
+    .toEqual({
+      'scope with trash': {
+        ok: 'value',
+      },
+      'scope with only trash': {},
+      'empty scope': {},
+      'normal scope': {
+        ok: 'value',
+      },
+      [$Errors]: [
+        new ParsingError('trash', 4),
+        new ParsingError('only trash', 7),
+      ],
+    });
+});

Vulnerable code:   "version": "1.1.5",
      "version": "3.4.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
      "integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==",
    "typescript": "3.4.3"
      "**/test/*.+(ts|tsx|js)"
export const $Errors: unique symbol = Symbol('Errors of parsing');

    super(`Unsupported type of line: [${lineNumber}]"${line}"`);
  (val: string, section: string, key: string): any
import { $Errors, ParsingError } from './errors';
          result[currentSection] = (isDataSection) ? [] : {};
import { parse, stringify } from '../src';
v2 = what 
v2 : what 
Fixed code:   "version": "1.2.0",
      "version": "4.1.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
      "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
    "typescript": "^4.1.3"
      "**/test/**/*.+(ts|tsx|js)"
export const $Errors: unique symbol = Symbol('Errors of parsing');
export * from './parsing-error';
export * from './proto-error';
export * from './errors-symbol';
    super(`Unsupported type of line: [${lineNumber}] "${line}"`);
export class ProtoError extends Error {
  constructor(lineNumber: number) {
    super(`Unsupported section name "__proto__": [${lineNumber}]"`);
    this.lineNumber = lineNumber;
  }

  public lineNumber: number;
}
export * from './proto';
  (val: string, section: string | symbol, key: string): any
import { $Proto } from '../proto';
  [$Proto]?: IIniObjectSection;
import {
  $Errors,
  ParsingError,
  ProtoError,
} from './errors';
import { $Proto } from './proto';
  protoSymbol?: boolean;
    protoSymbol = false,
        if (currentSection === '__proto__') {
          if (protoSymbol) {
            currentSection = <string><any> $Proto;
          } else {
            throw new ProtoError(lineNumber);
          }
        }
          result[currentSection] = (isDataSection) ? [] : Object.create(null);
export const $Proto: unique symbol = Symbol('__proto__');
import {
  parse,
  stringify,
  $Proto,
  $Errors,
  ParsingError,
} from '../src';
v2 = what

v2 : what




const ini6 = `
[ __proto__  ]
polluted = "polluted"`;

const ini7 = `
[scope with trash]
ok = value
trash

[scope with only trash]
only trash

[empty scope]
[normal scope]
ok = value
`;


test('ini parsing: proto', () => {
  expect(() => parse(ini6))
    .toThrow('Unsupported section name "__proto__": [2]"');

  expect(parse(ini6, { protoSymbol: true }))
    .toEqual({
      [$Proto]: {
        polluted: '"polluted"',
      },
    });
});

test('ini parsing: errors', () => {
  expect(() => parse(ini7))
    .toThrow('Unsupported type of line: [4] "trash"');

  expect(parse(ini7, { nothrow: true }))
    .toEqual({
      'scope with trash': {
        ok: 'value',
      },
      'scope with only trash': {},
      'empty scope': {},
      'normal scope': {
        ok: 'value',
      },
      [$Errors]: [
        new ParsingError('trash', 4),
        new ParsingError('only trash', 7),
      ],
    });
});