CVE-2021-23566: Incorrect Type Conversion or Cast (CWE-704)
value: The package nanoid from 3.0.0 and before 3.1.31 are vulnerable to Information Exposure via the valueOf() function which allows to reproduce the last id generated.
CWE description: The product does not correctly convert an object, resource, or structure from one type to a different type.
CVSS v3 base score: 5.5
Published: 2022-01-14T20:15Z
Repository: https://github.com/ai/nanoid
Language: JavaScript
Files changed: index.js, test/index.test.js
Commit date: 2022-01-11 14:36:07 +0100
Commit message: fix collision by proxy number (#328)


Diff: diff --git a/index.js b/index.js
index 19306b1..93ffabf 100644
--- a/index.js
+++ b/index.js
@@ -21,11 +21,12 @@ let fillPool = bytes => {
   }
   poolOffset += bytes
 }
 
 let random = bytes => {
-  fillPool(bytes)
+  // `-=` convert `bytes` to number to prevent `valueOf` abusing
+  fillPool((bytes -= 0))
   return pool.subarray(poolOffset - bytes, poolOffset)
 }
 
 let customRandom = (alphabet, size, getRandom) => {
   // First, a bitmask is necessary to generate the ID. The bitmask makes bytes
@@ -63,11 +64,12 @@ let customRandom = (alphabet, size, getRandom) => {
 }
 
 let customAlphabet = (alphabet, size) => customRandom(alphabet, size, random)
 
 let nanoid = (size = 21) => {
-  fillPool(size)
+  // `-=` convert `size` to number to prevent `valueOf` abusing
+  fillPool((size -= 0))
   let id = ''
   // We are reading directly from the random pool to avoid creating new array
   for (let i = poolOffset - size; i < poolOffset; i++) {
     // It is incorrect to use bytes exceeding the alphabet size.
     // The following mask reduces the random byte in the 0-255 value
diff --git a/test/index.test.js b/test/index.test.js
index 18043e0..2a6e91c 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -149,7 +149,43 @@ for (let type of ['node', 'browser']) {
           expect(byte).toBeLessThanOrEqual(255)
           expect(byte).toBeGreaterThanOrEqual(0)
         }
       })
     })
+
+    if (type === 'node') {
+      describe('proxy number', () => {
+        it('prevent collision', () => {
+          let makeProxyNumberToReproducePreviousID = () => {
+            let step = 0
+            return {
+              valueOf() {
+                // "if (!pool || pool.length < bytes) {"
+                if (step === 0) {
+                  step++
+                  return 0
+                }
+                // "} else if (poolOffset + bytes > pool.length) {"
+                if (step === 1) {
+                  step++
+                  return -Infinity
+                }
+                // "poolOffset += bytes"
+                if (step === 2) {
+                  step++
+                  return 0
+                }
+
+                return 21
+              }
+            }
+          }
+
+          let ID1 = nanoid()
+          let ID2 = nanoid(makeProxyNumberToReproducePreviousID())
+
+          expect(ID1).not.toBe(ID2)
+        })
+      })
+    }
   })
 }

Vulnerable code:   fillPool(bytes)
  fillPool(size)
Fixed code:   // `-=` convert `bytes` to number to prevent `valueOf` abusing
  fillPool((bytes -= 0))
  // `-=` convert `size` to number to prevent `valueOf` abusing
  fillPool((size -= 0))

    if (type === 'node') {
      describe('proxy number', () => {
        it('prevent collision', () => {
          let makeProxyNumberToReproducePreviousID = () => {
            let step = 0
            return {
              valueOf() {
                // "if (!pool || pool.length < bytes) {"
                if (step === 0) {
                  step++
                  return 0
                }
                // "} else if (poolOffset + bytes > pool.length) {"
                if (step === 1) {
                  step++
                  return -Infinity
                }
                // "poolOffset += bytes"
                if (step === 2) {
                  step++
                  return 0
                }

                return 21
              }
            }
          }

          let ID1 = nanoid()
          let ID2 = nanoid(makeProxyNumberToReproducePreviousID())

          expect(ID1).not.toBe(ID2)
        })
      })
    }