-
-
Notifications
You must be signed in to change notification settings - Fork 757
perf(urlencoded): optimize parameter counting for better memory efficiency #652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…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
6c95239 to
10958e6
Compare
|
Thank you for the contribution, this has already been resolved in our recent security patch. |
Hey @bjohansebas , just wanted to flag something about the 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 Quick example with
Or for
The issue is the 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. |
|
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: This actually is one parameter so both the old 1.x and the split based version were wrong. Same here 1.x and split based are wrong All our tests are written without the leading "&" so don't know if this is valid? This is the case that needs some fixup i think. But it is not critical as arrayLimit is set to 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
|
Hi @UlisesGascon, I’ve added the fix for parameterCount along with tests covering all edge cases |
|
Should it just defer to |
|
@bjohansebas @Phillip9587 as you can see I've implemented a fix again by removing the doWhile and added tests for it. |
Probably it would be good to delegate it to
It’s on my to-do list to review, but leave this PR open, there’s no need to open more PRs. |
|
I could implement a change in qs behind an option, that express sets - i'm unclear on what the change would be though. |
Hi @blakeembrey, I implemented your suggestion. Just to give some context, I had opened PR #652 back in October to fix the DoS issue with parameterCount that I mentioned in my earlier comment. @Phillip9587 committed the security patch (b204886) before my PR could be reviewed, which was good to get the fix out quickly. However, the implementation that got merged had a bug in the counting logic (the do-while loop issue I mentioned earlier, it counts iterations instead of actual Here's what I found: I refactored the code to defer parsing to qs as you suggested, but kept the early exit since it's important for DoS protection. The new implementation uses After implementing this, I realized the existing code was already doing most of what you suggested: it had early exit (qs never got called if limit exceeded), it was already deferring parsing to qs (since the default is false, qs doesn't check again), and the behavior was already optimal. I tried the pure deferral approach (removing the manual counting, using So the new code maintains the same optimal behavior as before, just with cleaner structure. It defers parsing to qs as you suggested, while keeping the early exit for performance.
@bjohansebas I did some quick timing tests and found that keeping the early exit is important. When the limit is exceeded, the pure deferral approach (using Regarding the "repeating work" concern, the existing code already had this optimization, and the new code keeps it while still deferring parsing to qs. So we're not really duplicating work: we do a quick pre-check for early exit, then qs handles all the parsing. If you'd like, I can run more thorough benchmarks to quantify the difference.
@ljharb Thank you for offering! After implementing this, I don't think we need any changes to qs. The existing What do you all think? Is keeping the early exit while deferring parsing what you had in mind, or would you prefer going full deferral despite the performance trade-off? |
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:
parameterLimitoption.This particularly improves resilience against malicious requests with thousands of parameters attempting to exhaust server resources.