Vulnerability: Resource exhaustion in socket.io-parser The `socket.io-parser` npm package before versions 3.3.2 and 3.4.1 allows attackers to cause a denial of service (memory consumption) via a large packet because a concatenation approach is used.
Vulnerability ID: GHSA-xfhh-g9f5-x4m4
CWE: CWE-400
Severity score: 7.5
Project: https://github.com/socketio/socket.io-parser
Published: 2021-06-30
File: index.js
Commit message: fix: prevent DoS (OOM) via massive packets (#95)

When maxHttpBufferSize is large (1e8 bytes), a payload of length 100MB
can be sent like so:

99999991:422222222222222222222222222222222222222222222...

This massive packet can cause OOM via building up many many
`ConsOneByteString` objects due to concatenation:
99999989 `ConsOneByteString`s and then converting the massive integer to
a `Number`.

The performance can be improved to avoid this by using `substring`
rather than building the string via concatenation.

Below I tried one payload of length 7e7 as the 1e8 payload took so
long to process that it timed out before running out of memory.

```
==== JS stack trace =========================================

    0: ExitFrame [pc: 0x13c5b79]
Security context: 0x152fe7b808d1 <JSObject>
    1: decodeString [0x2dd385fb5d1] [/node_modules/socket.io-parser/index.js:~276] [pc=0xf59746881be](this=0x175d34c42b69 <JSGlobal Object>,0x14eccff10fe1 <Very long string[69999990]>)
    2: add [0x31fc2693da29] [/node_modules/socket.io-parser/index.js:242] [bytecode=0xa7ed6554889 offset=11](this=0x0a2881be5069 <Decoder map = 0x3ceaa8bf48c9>,0x14eccff10fe1 <Very...

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0xa09830 node::Abort() [node]
 2: 0xa09c55 node::OnFatalError(char const*, char const*) [node]
 3: 0xb7d71e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
 4: 0xb7da99 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
 5: 0xd2a1f5  [node]
 6: 0xd2a886 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [node]
 7: 0xd37105 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
 8: 0xd37fb5 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
 9: 0xd3965f v8::internal::Heap::HandleGCRequest() [node]
10: 0xce8395 v8::internal::StackGuard::HandleInterrupts() [node]
11: 0x1042cb6 v8::internal::Runtime_StackGuard(int, unsigned long*, v8::internal::Isolate*) [node]
12: 0x13c5b79  [node]
```
Commit author: bcaller

Vulnerable version (before fix):
 function decodeString(str) {    // look up attachments if type binary   if (exports.BINARY_EVENT === p.type || exports.BINARY_ACK === p.type) {     var buf = '';     while (str.charAt(++i) !== '-') {       buf += str.charAt(i);       if (i == str.length) break;     }     if (buf != Number(buf) || str.charAt(i) !== '-') {       throw new Error('Illegal attachments');     } function decodeString(str) {    // look up namespace (if any)   if ('/' === str.charAt(i + 1)) {     p.nsp = '';     while (++i) {       var c = str.charAt(i);       if (',' === c) break;       p.nsp += c;       if (i === str.length) break;     }   } else {     p.nsp = '/';   }    // look up id   var next = str.charAt(i + 1);   if ('' !== next && Number(next) == next) {     p.id = '';     while (++i) {       var c = str.charAt(i);       if (null == c || Number(c) != c) {         --i;         break;       }       p.id += str.charAt(i);       if (i === str.length) break;     }     p.id = Number(p.id);   }    // look up json data

Fixed version (after fix):
 function decodeString(str) {    // look up attachments if type binary   if (exports.BINARY_EVENT === p.type || exports.BINARY_ACK === p.type) {     var start = i + 1;     while (str.charAt(++i) !== '-' && i != str.length) {}     var buf = str.substring(start, i);     if (buf != Number(buf) || str.charAt(i) !== '-') {       throw new Error('Illegal attachments');     } function decodeString(str) {    // look up namespace (if any)   if ('/' === str.charAt(i + 1)) {     var start = i + 1;     while (++i) {       var c = str.charAt(i);       if (',' === c) break;       if (i === str.length) break;     }     p.nsp = str.substring(start, i);   } else {     p.nsp = '/';   }    // look up id   var next = str.charAt(i + 1);   if ('' !== next && Number(next) == next) {     var start = i + 1;     while (++i) {       var c = str.charAt(i);       if (null == c || Number(c) != c) {         --i;         break;       }       if (i === str.length) break;     }     p.id = Number(str.substring(start, i + 1));   }    // look up json data

Patch:
patch: @@ -286,11 +286,9 @@ function decodeString(str) {
 
   // look up attachments if type binary
   if (exports.BINARY_EVENT === p.type || exports.BINARY_ACK === p.type) {
-    var buf = '';
-    while (str.charAt(++i) !== '-') {
-      buf += str.charAt(i);
-      if (i == str.length) break;
-    }
+    var start = i + 1;
+    while (str.charAt(++i) !== '-' && i != str.length) {}
+    var buf = str.substring(start, i);
     if (buf != Number(buf) || str.charAt(i) !== '-') {
       throw new Error('Illegal attachments');
     }
@@ -299,31 +297,30 @@ function decodeString(str) {
 
   // look up namespace (if any)
   if ('/' === str.charAt(i + 1)) {
-    p.nsp = '';
+    var start = i + 1;
     while (++i) {
       var c = str.charAt(i);
       if (',' === c) break;
-      p.nsp += c;
       if (i === str.length) break;
     }
+    p.nsp = str.substring(start, i);
   } else {
     p.nsp = '/';
   }
 
   // look up id
   var next = str.charAt(i + 1);
   if ('' !== next && Number(next) == next) {
-    p.id = '';
+    var start = i + 1;
     while (++i) {
       var c = str.charAt(i);
       if (null == c || Number(c) != c) {
         --i;
         break;
       }
-      p.id += str.charAt(i);
       if (i === str.length) break;
     }
-    p.id = Number(p.id);
+    p.id = Number(str.substring(start, i + 1));
   }
 
   // look up json data
path: index.js