Skip to content

Conversation

@Ayoub-Mabrouk
Copy link
Contributor

@Ayoub-Mabrouk Ayoub-Mabrouk commented Oct 29, 2025

The previous implementation used body.split('&') which always processed the entire request body and allocated a full array, regardless of the parameter limit.

The new implementation:

  • Counts '&' characters iteratively without array allocation
  • Exits immediately when the limit is reached
  • Reduces time complexity from O(n) worst-case always to O(min(n, limit))
  • Added test case to verify empty body handling with parameterLimit option.

This particularly improves resilience against malicious requests with thousands of parameters attempting to exhaust server resources.

…iency

The previous implementation used �ody.split('&') which always
processed the entire request body and allocated a full array,
regardless of the parameter limit.

The new implementation:
- Counts '&' characters iteratively without array allocation
- Exits immediately when the limit is reached
- Handles edge case of empty/null body
- Reduces time complexity from O(n) worst-case always to O(min(n, limit))

This particularly improves resilience against malicious requests
with thousands of parameters attempting to exhaust server resources
@bjohansebas
Copy link
Member

Thank you for the contribution, this has already been resolved in our recent security patch.

GHSA-wqch-xfxh-vrr4

@Ayoub-Mabrouk
Copy link
Contributor Author

Ayoub-Mabrouk commented Nov 25, 2025

Thank you for the contribution, this has already been resolved in our recent security patch.

GHSA-wqch-xfxh-vrr4

Hey @bjohansebas , just wanted to flag something about the parameterCount fix that went into the security patch.

I had opened PR #652 back on Oct 29 to fix this exact issue - basically the same DoS vulnerability you patched. My version kept the original behavior while adding the early exit logic.

The thing is, the implementation that got merged (commit b204886) doesn't actually return the same values as the old code did.

Quick example with "a=1&b=2&c=3":

  • Old version returned 2
  • My PR also returned 2
  • Current patch returns 4

Or for "a=1" with no ampersands:

  • Old version returned 0
  • My PR returned 0
  • Current patch returns 1

The issue is the do-while loop always runs at least once and counts iterations instead of actual & characters, so the numbers are off.

Not trying to reopen my PR or anything, just thought someone should know since this changes behavior that might be relied on elsewhere. Could cause weird issues down the line even though it's technically internal.

@bjohansebas bjohansebas reopened this Nov 25, 2025
@Phillip9587
Copy link
Member

Hey @Ayoub-Mabrouk, you are right. The security fix implementation is off by 1 compared to the original implementation. But it is actually more correct then the old implementation with some small exceptions:

"user=bob"
    2.2.1: 1
    1.20.3: 0
    2.2.0: 0

This actually is one parameter so both the old 1.x and the split based version were wrong.

"a=1&b=2&c=3"
    2.2.1: 3
    1.20.3: 2
    2.2.0: 2

Same here 1.x and split based are wrong

"&a=1&b=2&c=3"
    2.2.1: 4
    1.20.3: 3
    2.2.0: 3

All our tests are written without the leading "&" so don't know if this is valid?

""
    2.2.1: 1
    1.20.3: 0
    2.2.0: 0

This is the case that needs some fixup i think. But it is not critical as arrayLimit is set to Math.max(100, parameterCount).

I already talked to @UlisesGascon and he suggested opening a PR which adds tests and documentation for the expected behaviour of this function.

- Fix parameterCount to correctly count parameters (ampersands + 1)
- Handle empty string edge case (returns 0, not 1)
- Optimize using indexOf instead of character iteration
- Add comprehensive tests documenting expected behavior
- Address edge cases: leading/trailing ampersands, consecutive ampersands
@Ayoub-Mabrouk
Copy link
Contributor Author

Hi @UlisesGascon, I’ve added the fix for parameterCount along with tests covering all edge cases

@blakeembrey
Copy link
Member

blakeembrey commented Nov 25, 2025

Should it just defer to qs and handle the error by enabling this option? https://github.com/ljharb/qs/blob/e8b32388dd8d095def77f5f21d3fdb7ca0c49cbc/lib/parse.js#L72-L74

@Ayoub-Mabrouk
Copy link
Contributor Author

@bjohansebas @Phillip9587 as you can see I've implemented a fix again by removing the doWhile and added tests for it.
Is there any feedback on this? Or should this change appear on another pr?

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.

4 participants