CVE-2020-7788: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: This affects the package ini before 1.3.6. If an attacker submits a malicious INI file to an application that parses it with ini.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: 2020-12-11T11:15Z
Repository: https://github.com/npm/ini
Language: JavaScript
Files changed: ini.js, test/proto.js
Commit date: 2020-12-08 14:35:17 -0800
Commit message: do not allow invalid hazardous string as section name

Diff: diff --git a/ini.js b/ini.js
index 590195d..0401258 100644
--- a/ini.js
+++ b/ini.js
@@ -78,10 +78,16 @@ function decode (str) {
     if (!line || line.match(/^\s*[;#]/)) return
     var match = line.match(re)
     if (!match) return
     if (match[1] !== undefined) {
       section = unsafe(match[1])
+      if (section === '__proto__') {
+        // not allowed
+        // keep parsing the section, but don't attach it.
+        p = {}
+        return
+      }
       p = out[section] = out[section] || {}
       return
     }
     var key = unsafe(match[2])
     var value = match[3] ? unsafe(match[4]) : true
@@ -92,10 +98,11 @@ function decode (str) {
     }
 
     // Convert keys with '[]' suffix to an array
     if (key.length > 2 && key.slice(-2) === '[]') {
       key = key.substring(0, key.length - 2)
+      if (key === '__proto__') return
       if (!p[key]) {
         p[key] = []
       } else if (!Array.isArray(p[key])) {
         p[key] = [p[key]]
       }
@@ -123,10 +130,11 @@ function decode (str) {
     var parts = dotSplit(k)
     var p = out
     var l = parts.pop()
     var nl = l.replace(/\\\./g, '.')
     parts.forEach(function (part, _, __) {
+      if (part === '__proto__') return
       if (!p[part] || typeof p[part] !== 'object') p[part] = {}
       p = p[part]
     })
     if (p === out && nl === l) {
       return false
diff --git a/test/proto.js b/test/proto.js
new file mode 100644
index 0000000..ab35533
--- /dev/null
+++ b/test/proto.js
@@ -0,0 +1,45 @@
+var ini = require('../')
+var t = require('tap')
+
+var data = `
+__proto__ = quux
+foo = baz
+[__proto__]
+foo = bar
+[other]
+foo = asdf
+[kid.__proto__.foo]
+foo = kid
+[arrproto]
+hello = snyk
+__proto__[] = you did a good job
+__proto__[] = so you deserve arrays
+thanks = true
+`
+var res = ini.parse(data)
+t.deepEqual(res, {
+  foo: 'baz',
+  other: {
+    foo: 'asdf',
+  },
+  kid: {
+    foo: {
+      foo: 'kid',
+    },
+  },
+  arrproto: {
+    hello: 'snyk',
+    thanks: true,
+  },
+})
+t.equal(res.__proto__, Object.prototype)
+t.equal(res.kid.__proto__, Object.prototype)
+t.equal(res.kid.foo.__proto__, Object.prototype)
+t.equal(res.arrproto.__proto__, Object.prototype)
+t.equal(Object.prototype.foo, undefined)
+t.equal(Object.prototype[0], undefined)
+t.equal(Object.prototype['0'], undefined)
+t.equal(Object.prototype[1], undefined)
+t.equal(Object.prototype['1'], undefined)
+t.equal(Array.prototype[0], undefined)
+t.equal(Array.prototype[1], undefined)

Fixed code:       if (section === '__proto__') {
        // not allowed
        // keep parsing the section, but don't attach it.
        p = {}
        return
      }
      if (key === '__proto__') return
      if (part === '__proto__') return
var ini = require('../')
var t = require('tap')

var data = `
__proto__ = quux
foo = baz
[__proto__]
foo = bar
[other]
foo = asdf
[kid.__proto__.foo]
foo = kid
[arrproto]
hello = snyk
__proto__[] = you did a good job
__proto__[] = so you deserve arrays
thanks = true
`
var res = ini.parse(data)
t.deepEqual(res, {
  foo: 'baz',
  other: {
    foo: 'asdf',
  },
  kid: {
    foo: {
      foo: 'kid',
    },
  },
  arrproto: {
    hello: 'snyk',
    thanks: true,
  },
})
t.equal(res.__proto__, Object.prototype)
t.equal(res.kid.__proto__, Object.prototype)
t.equal(res.kid.foo.__proto__, Object.prototype)
t.equal(res.arrproto.__proto__, Object.prototype)
t.equal(Object.prototype.foo, undefined)
t.equal(Object.prototype[0], undefined)
t.equal(Object.prototype['0'], undefined)
t.equal(Object.prototype[1], undefined)
t.equal(Object.prototype['1'], undefined)
t.equal(Array.prototype[0], undefined)
t.equal(Array.prototype[1], undefined)