Critical severityOSV Advisory· Published Jul 25, 2022· Updated Sep 16, 2024
Prototype Pollution
CVE-2020-28461
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
js-ininpm | < 1.3.0 | 1.3.0 |
Affected products
1Patches
112 files changed · +104 −15
package.json+2 −2 modified@@ -38,7 +38,7 @@ "husky": "^4.2.3", "jest": "^22.4.3", "ts-jest": "^22.4.2", - "typescript": "3.4.3" + "typescript": "^4.1.3" }, "jest": { "globals": { @@ -50,7 +50,7 @@ "^.+\\.(ts|tsx)$": "ts-jest" }, "testMatch": [ - "**/test/*.+(ts|tsx|js)" + "**/test/**/*.+(ts|tsx|js)" ], "moduleFileExtensions": [ "ts",
package-lock.json+4 −4 modified@@ -1,6 +1,6 @@ { "name": "js-ini", - "version": "1.1.5", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8362,9 +8362,9 @@ "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": {
src/errors/errors-symbol.ts+1 −0 added@@ -0,0 +1 @@ +export const $Errors: unique symbol = Symbol('Errors of parsing');
src/errors/index.ts+3 −0 added@@ -0,0 +1,3 @@ +export * from './parsing-error'; +export * from './proto-error'; +export * from './errors-symbol';
src/errors/parsing-error.ts+1 −3 renamed@@ -1,8 +1,6 @@ -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; }
src/errors/proto-error.ts+8 −0 added@@ -0,0 +1,8 @@ +export class ProtoError extends Error { + constructor(lineNumber: number) { + super(`Unsupported section name "__proto__": [${lineNumber}]"`); + this.lineNumber = lineNumber; + } + + public lineNumber: number; +}
src/index.ts+1 −0 modified@@ -1,3 +1,4 @@ export * from './errors'; export * from './parse'; export * from './stringify'; +export * from './proto';
src/interfaces/custom-typing.ts+1 −1 modified@@ -1,3 +1,3 @@ export interface ICustomTyping { - (val: string, section: string, key: string): any + (val: string, section: string | symbol, key: string): any }
src/interfaces/ini-object.ts+2 −0 modified@@ -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; }
src/parse.ts+16 −2 modified@@ -1,16 +1,22 @@ -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 = /\[(.*)]$/; @@ -22,6 +28,7 @@ export function parse(data: string, params?: IParseConfig): IIniObject { nothrow = false, autoTyping = true, dataSections = [], + protoSymbol = false, } = { ...params }; let typeParser: ICustomTyping; if (typeof autoTyping === 'function') { @@ -45,9 +52,16 @@ export function parse(data: string, params?: IParseConfig): IIniObject { 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; }
src/proto.ts+1 −0 added@@ -0,0 +1 @@ +export const $Proto: unique symbol = Symbol('__proto__');
test/tests.ts+64 −3 modified@@ -1,27 +1,35 @@ -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 @@ -33,6 +41,7 @@ v5=who is who = who [test scope with spaces] mgm*1=2.5`; + const ini4 = `v1: 2 v-2: true v 3: string @@ -42,6 +51,7 @@ 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 @@ -53,6 +63,24 @@ v1: 5 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, @@ -153,3 +181,36 @@ test('ini stringify', () => { 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), + ], + }); +});
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-m939-vrfp-9v8pghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-28461ghsaADVISORY
- github.com/Sdju/js-ini/commit/fa17efb7e3a7c9464508a254838d4c231784931eghsax_refsource_MISCWEB
- security.snyk.io/vuln/SNYK-JS-JSINI-1048970ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.