Skip to content

Commit

Permalink
Fix pool pollution, infinite loop (#510)
Browse files Browse the repository at this point in the history
* Fix pool pollution, infinite loop

When nanoid is called with a fractional value, there were a number
of undesirable effects:
- in browser and non-secure, the code infinite loops on `while (size--)`
- in node, the value of poolOffset becomes fractional, causing calls to
  nanoid to return zeroes until the pool is next filled: when `i` is
  initialized to `poolOffset`, `pool[i] & 63` -> `undefined & 63` -> `0`
- if the first call in node is a fractional argument, the initial buffer
  allocation fails with an error

I chose `|0` to cast to a signed integer primarily because that has a
slightly better outcome in the third case above: if the first call is
negative (e.g. `nanoid(-1)`) then Node will throw an error for an
invalid Buffer size, rather than attempting to allocate a buffer of
size `2**32-1`. It's also more compact than `>>>0`, which would be
necessary to cast to an unsigned integer. I don't _think_ there is
a use case for generating ids longer than `2**31-1` :)

The browser code is structured in such a way that casting `size` in
`customRandom` succinctly isn't readily feasible. I chose to cast it
at the line `let j = step | 0` since casting defaultSize would not
fix the infinite loop in all cases, and the other use of defaultSize
is to define the step length which is already shown to be fractional
and gets cast to an integer with `~` anyway.

As for the `nanoid` function, `new Uint8Array(size)` ignores the
fractional part, and `size` doesn't get used further - the function
instead calls reduce over the typed array.

In the Node/native async customAlphabet variant, I chose to convert
the `id.length === size` check to `id.length >= size`, which handles
the fractional case and avoids the infinite loop; `size` is not used
for anything else there.

* `const` -> `let`
  • Loading branch information
myndzi authored Nov 26, 2024
1 parent 89d82d2 commit d643045
Show file tree
Hide file tree
Showing 10 changed files with 55 additions and 17 deletions.
4 changes: 2 additions & 2 deletions async/index.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {
while (true) {
let bytes = crypto.getRandomValues(new Uint8Array(step))
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = step
let i = step | 0
while (i--) {
// Adding `|| ''` refuses a random byte that exceeds the alphabet size.
id += alphabet[bytes[i] & mask] || ''
Expand All @@ -41,7 +41,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {

let nanoid = async (size = 21) => {
let id = ''
let bytes = crypto.getRandomValues(new Uint8Array(size))
let bytes = crypto.getRandomValues(new Uint8Array((size |= 0)))

// A compact alternative for `for (var i = 0; i < step; i++)`.
while (size--) {
Expand Down
4 changes: 2 additions & 2 deletions async/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {
while (i--) {
// Adding `|| ''` refuses a random byte that exceeds the alphabet size.
id += alphabet[bytes[i] & mask] || ''
if (id.length === size) return id
if (id.length >= size) return id
}
return tick(id, size)
})
Expand All @@ -54,7 +54,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {
}

let nanoid = (size = 21) =>
random(size).then(bytes => {
random((size |= 0)).then(bytes => {
let id = ''
// A compact alternative for `for (var i = 0; i < step; i++)`.
while (size--) {
Expand Down
4 changes: 2 additions & 2 deletions async/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {
while (i--) {
// Adding `|| ''` refuses a random byte that exceeds the alphabet size.
id += alphabet[bytes[i] & mask] || ''
if (id.length === size) return id
if (id.length >= size) return id
}
return tick(id, size)
})
Expand All @@ -40,7 +40,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {
}

let nanoid = (size = 21) =>
random(size).then(bytes => {
random((size |= 0)).then(bytes => {
let id = ''
// A compact alternative for `for (var i = 0; i < step; i++)`.
while (size--) {
Expand Down
2 changes: 1 addition & 1 deletion index.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ let customRandom = (alphabet, defaultSize, getRandom) => {
while (true) {
let bytes = getRandom(step)
// A compact alternative for `for (var i = 0; i < step; i++)`.
let j = step
let j = step | 0
while (j--) {
// Adding `|| ''` refuses a random byte that exceeds the alphabet size.
id += alphabet[bytes[j] & mask] || ''
Expand Down
8 changes: 4 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ let fillPool = bytes => {
}

let random = bytes => {
// `-=` convert `bytes` to number to prevent `valueOf` abusing
fillPool((bytes -= 0))
// `|=` convert `bytes` to number to prevent `valueOf` abusing and pool pollution
fillPool((bytes |= 0))
return pool.subarray(poolOffset - bytes, poolOffset)
}

Expand Down Expand Up @@ -67,8 +67,8 @@ let customAlphabet = (alphabet, size = 21) =>
customRandom(alphabet, size, random)

let nanoid = (size = 21) => {
// `-=` convert `size` to number to prevent `valueOf` abusing
fillPool((size -= 0))
// `|=` convert `size` to number to prevent `valueOf` abusing and pool pollution
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++) {
Expand Down
4 changes: 2 additions & 2 deletions non-secure/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {
return (size = defaultSize) => {
let id = ''
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = size
let i = size | 0
while (i--) {
// `| 0` is more compact and faster than `Math.floor()`.
id += alphabet[(Math.random() * alphabet.length) | 0]
Expand All @@ -23,7 +23,7 @@ let customAlphabet = (alphabet, defaultSize = 21) => {
let nanoid = (size = 21) => {
let id = ''
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = size
let i = size | 0
while (i--) {
// `| 0` is more compact and faster than `Math.floor()`.
id += urlAlphabet[(Math.random() * 64) | 0]
Expand Down
9 changes: 8 additions & 1 deletion test/async.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
let { suite } = require('uvu')
let { spy } = require('nanospy')
let { is, match, ok } = require('uvu/assert')
let { is, match, ok, not } = require('uvu/assert')
let crypto = require('crypto')

global.crypto = {
Expand Down Expand Up @@ -55,6 +55,13 @@ for (let type of ['node', 'browser']) {
)
})

nanoidSuite('avoids pool pollution, infinite loop', async () => {
await nanoid(2.1)
let second = await nanoid()
let third = await nanoid()
not.equal(second, third)
})

nanoidSuite('changes ID length', async () => {
let id = await nanoid(10)
is(id.length, 10)
Expand Down
9 changes: 8 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
let { test } = require('uvu')
let { is, ok, equal, match } = require('uvu/assert')
let { is, ok, equal, match, not } = require('uvu/assert')

let browser = require('../index.browser.js')
let node = require('../index.js')
Expand Down Expand Up @@ -34,6 +34,13 @@ for (let type of ['node', 'browser']) {
}
})

test(`${type} / nanoid / avoids pool pollution, infinite loop`, () => {
nanoid(2.1)
let second = nanoid()
let third = nanoid()
not.equal(second, third)
})

test(`${type} / nanoid / changes ID length`, () => {
is(nanoid(10).length, 10)
})
Expand Down
18 changes: 17 additions & 1 deletion test/non-secure.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
let { test } = require('uvu')
let { is, match, ok } = require('uvu/assert')
let { is, match, ok, not } = require('uvu/assert')

let { nanoid, customAlphabet } = require('../non-secure')
let { urlAlphabet } = require('..')
Expand Down Expand Up @@ -56,6 +56,13 @@ test('nanoid / has flat distribution', () => {
ok(max - min <= 0.05)
})

test('nanoid / avoids pool pollution, infinite loop', () => {
nanoid(2.1)
let second = nanoid()
let third = nanoid()
not.equal(second, third)
})

test('customAlphabet / has options', () => {
let nanoidA = customAlphabet('a', 5)
is(nanoidA(), 'aaaaa')
Expand Down Expand Up @@ -88,4 +95,13 @@ test('customAlphabet / has flat distribution', () => {
ok(max - min <= 0.05)
})

test('customAlphabet / avoids pool pollution, infinite loop', () => {
let ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
let nanoid2 = customAlphabet(ALPHABET)
nanoid2(2.1)
let second = nanoid2()
let third = nanoid2()
not.equal(second, third)
})

test.run()
10 changes: 9 additions & 1 deletion test/react-native-polyfill.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
let { test } = require('uvu')
let { is } = require('uvu/assert')
let { is, not } = require('uvu/assert')

test.before(() => {
global.navigator = {
Expand Down Expand Up @@ -27,4 +27,12 @@ test('works with polyfill', () => {
is(typeof nanoid(), 'string')
})

test('nanoid / avoids pool pollution, infinite loop', () => {
let { nanoid } = require('../index.browser')
nanoid(2.1)
let second = nanoid()
let third = nanoid()
not.equal(second, third)
})

test.run()

0 comments on commit d643045

Please sign in to comment.