Skip to content

fix: correct off-by-one error in parameterCount#716

Open
abhu85 wants to merge 1 commit intoexpressjs:1.xfrom
abhu85:fix/off-by-one-parameterCount-715
Open

fix: correct off-by-one error in parameterCount#716
abhu85 wants to merge 1 commit intoexpressjs:1.xfrom
abhu85:fix/off-by-one-parameterCount-715

Conversation

@abhu85
Copy link
Copy Markdown

@abhu85 abhu85 commented Mar 30, 2026

Summary

Fix off-by-one error in parameterCount that causes large arrays to be parsed as objects with qs@6.14.2.

Problem

The parameterCount function counts & characters but returns that count directly. Since the number of parameters equals the number of & characters plus one, this causes an off-by-one error.

For example, with a[0]=1&a[1]=2&...&a[199]=200 (200 parameters):

  • Before fix: returned 199 (count of & chars)
  • After fix: returns 200 (actual parameter count)

This became problematic with qs@6.14.2 which changed arrayLimit behavior to restrict array length instead of max index.

Solution

Change return count to return count + 1 in parameterCount.

Test Plan

  • Added test case for array parsing with 110 elements
  • All 263 tests pass
  • Linting passes

Compatibility

  • No breaking changes
  • Backwards compatible (arrays that worked before still work)

Fixes #715

The `parameterCount` function was counting `&` characters but returning
that count directly. Since the number of parameters equals the number
of `&` characters plus one, this caused an off-by-one error.

For example, with `a[0]=1&a[1]=2&...&a[199]=200` (200 parameters):
- Before: returned 199 (number of `&` chars)
- After: returns 200 (actual parameter count)

This bug became visible with qs@6.14.2 which changed `arrayLimit` to
restrict array length instead of max index. With the buggy count,
large arrays would be converted to objects instead of arrays.

- Fix `parameterCount` to return `count + 1`
- Add test case for array parsing with 110 elements

Fixes expressjs#715
Comment on lines +509 to +533

it('should correctly count parameters for array parsing', function (done) {
// Test for off-by-one bug fix (issue #715)
// With 110 array elements, there are 110 parameters (109 & chars)
// Before fix: parameterCount returned 109, arrayLimit was 109
// After fix: parameterCount returns 110, arrayLimit is 110
var server = createServer({ extended: true, parameterLimit: 200 })
var arrayParams = Array.from({ length: 110 }, function (_, i) {
return 'a[' + i + ']=' + (i + 1)
}).join('&')

request(server)
.post('/')
.type('form')
.send(arrayParams)
.expect(function (res) {
// The body should contain an array 'a' with 110 elements
// If parameterCount returns 109 (bug), arrayLimit would be 109,
// and qs would convert indices >= 109 to object keys
var body = JSON.parse(res.text)
assert.ok(Array.isArray(body.a), 'should have array "a", got: ' + res.text.substring(0, 200))
assert.strictEqual(body.a.length, 110, 'array should have 110 elements')
})
.expect(200, done)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There already is a test that verifies this

it('should parse array index notation with large array', function (done) {
var str = 'f[0]=0'
for (var i = 1; i < 500; i++) {
str += '&f[' + i + ']=' + i.toString(16)
}
request(this.server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(str)
.expect(function (res) {
var obj = JSON.parse(res.text)
assert.strictEqual(Object.keys(obj).length, 1)
assert.strictEqual(Array.isArray(obj.f), true)
assert.strictEqual(obj.f.length, 500)
})
.expect(200, done)
})

@abhu85
Copy link
Copy Markdown
Author

abhu85 commented Mar 31, 2026

Thanks for pointing that out. I see the existing test at L167-185 with 500 elements.

I added my test because I wanted to verify the specific off-by-one fix, but you're right that the existing test should theoretically catch this too since:

  • 500 parameters → paramCount returns 499 (bug)
  • arrayLimit = Max.max(100, 499) = 499
  • qs 6.14.2+ would convert index 499 to object key

Does the existing test currently fail on main without this fix? If so, I'm happy to remove my test as redundant. If there's something causing it to pass despite the bug, my test provides additional coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants