SevyDevy

Guide · Backend

Body-Parser's October 2026 Vulnerability Explained: Upgrade to 2.4.0 or 1.20.9

21 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The body parser vulnerability lets one compressed request crash a Node.js server that uses body-parser on default settings. body-parser is the library behind express.json(). Security tools list it as GHSA-27r3-jxmh-p828.
  • The cause is a missing error listener, a function that handles a failure. body-parser stops listening for errors on its decompression stream, the part that unzips the body, while bytes can still reach it.
  • The fix is body-parser 2.4.0 for Express 5 and 1.20.9 for Express 4, both on npm since 22 October 2026. Express 4.16.0 to 4.21.2 pin an exact old body-parser, so npm update body-parser alone misses it.
  • If you cannot deploy today, set inflate: false on each body-parser parser you use. Compressed request bodies then get a 415 Unsupported Media Type error before any unzipping starts.
  • Beginners can stop after the crash section and one version check. Engineers get the fix read line by line, the lockfile trap with tested npm commands and how to survive the next crash bug.

What goes wrong when one request can stop a Node.js server?

The whole server process stops, and every request it was handling fails with it. That is what the body parser vulnerability fixed in October 2026 allows. One compressed request with extra bytes after the compressed data can end the process.

No login is needed. If your app adds express.json() for every path with app.use(), the request does not even need to match one of your routes. body-parser reads the body before Express picks a route handler.

Picture a common setup. A food delivery startup in Bengaluru runs its order API on Express 4, as three Node.js processes behind a load balancer. Its lockfile was last refreshed in early 2025, so it still installs Express 4.21.2. The setup is made up, but the version trap it shows is real.

If one of those processes receives the bad request, it exits. The load balancer moves traffic to the other two until the attacker sends the same request to them as well. A supervisor can restart each process, but nothing stops the next request.

What is the body parser vulnerability fixed in October 2026?

It is a denial-of-service bug in the npm package body-parser. Denial of service means an attacker stops your service from answering anyone. In this bug, that is all they can do: they cannot read or change your data.

body-parser is the package that reads a request body and turns it into req.body. Express hands it to you as express.json(), express.urlencoded(), express.text() and express.raw(). We checked that express.json is the same function as body-parser's json in Express 4.22.3 and 5.3.0.

The bug has two IDs you will see in security tools. GHSA-27r3-jxmh-p828 is GitHub's ID, and CVE-2026-111181 is the shared public ID. Its severity score, called CVSS, is 7.5 out of 10, rated High.

GHSA-27r3-jxmh-p828 at a glance, as of 25 October 2026
FieldValue
Advisory and CVEGHSA-27r3-jxmh-p828, CVE-2026-111181
SeverityHigh, 7.5 out of 10 (CVSS 3.1): works over the network, needs no login or user action, and only stops the service
Kind of bugAn uncaught exception, listed as CWE-248
Affectedbody-parser below 1.20.9, and 2.0.0 up to but not including 2.4.0
Fixed1.20.9 and 2.4.0, published to npm on 22 October 2026
TriggerA compressed request body followed by trailing bytes, with default options
Workaroundinflate: false, if you do not need compressed request bodies

Which words do you need before following a request through body-parser?

Each term below gets a plain meaning here and keeps that name for the rest of the guide.

  • Middleware: a function Express runs on a request before your route code. express.json() is middleware.
  • Stream: data that arrives in pieces over time. A request body uploaded in chunks is a stream.
  • Pipe: a link from one stream to another, so every piece that arrives on the first is written into the second.
  • Content-Encoding: a request header that says the body is compressed, for example gzip or deflate. Compression makes the body smaller to send.
  • Decompression stream, or decoder: a stream that takes compressed pieces in and gives the original bytes out. Node.js builds these with its zlib module.
  • Event and listener: Node.js objects announce things by emitting named events such as data, end or error. A listener is a function attached to hear one event.
  • Uncaught exception: an error that no code catches. By default Node.js prints the stack trace and exits with code 1.

Why does an unhandled error event crash a Node.js process?

Because Node.js treats the error event as special. The Node.js events documentation says that if an emitter has no error listener and emits error, the error is thrown, a stack trace is printed and the process exits.

Every stream is an event emitter, so this rule covers sockets, files and zlib decoders too. You can watch it happen in a few lines. This demo touches nothing outside its own process.

javascript
const { EventEmitter } = require('node:events')

const decoder = new EventEmitter()

if (process.argv[2] === 'listener') {
  decoder.on('error', (err) => console.log('handled:', err.message))
}

decoder.emit('error', new Error('decoder failed'))
console.log('still running')

Save it as demo.js. Run node demo.js and you get a stack trace with the note Unhandled 'error' event, then exit code 1. Run node demo.js listener and it prints handled: decoder failed, then still running, with exit code 0.

What just happened? Both runs emitted the same error. With no listener, Node.js threw it and stopped the process. With one, the listener logged it and the program carried on. We ran both on Node.js 22.23.4.

A picture for the rule

Think of an office with one strict rule: an emergency call must be answered, or the whole building is evacuated. The error event is the emergency call. The listener is the person assigned to pick it up.

The process exit is the evacuation. Everyone in the building goes home, not just the person who caused the call. In Node.js terms, every request on that process fails, not just the bad one.

Where the picture breaks: there is no ringing and no waiting. The throw happens at once, inside the emit call, so nobody gets a chance to walk over.

Why try/catch does not save you

Pause and predict: if you wrap your code in try/catch, will the process survive the decoder's error? Decide before you read on.

It will not. The decoder emits its error later, from inside the stream, after your try block has finished. We tested a try/catch around an error emitted one tick later, and the process still exited with code 1. A watch-only listener added with events.errorMonitor does not help either.

How does a compressed request body reach the crash inside body-parser?

body-parser pipes the request into a decoder, reads the decoder until the compressed data ends, then removes its error listener. Bytes that arrive after that reach a finished decoder with nobody listening.

We read this path in body-parser 2.3.0's lib/read.js and in raw-body 3.0.2, the helper that collects the body. The 1.20.8 code follows the same steps with raw-body 2.5.3.

  1. Pick a decoder

    read.js checks the Content-Encoding header. For gzip it creates a zlib gunzip stream and runs req.pipe(stream), so request bytes flow into the decoder.

  2. Read until the decoder ends

    raw-body adds data, end and error listeners to the decoder. A gzip body carries its own end marker, so the decoder can end before the request does.

  3. Clean up the listeners

    When the decoder ends, raw-body's cleanup function removes its listeners, the error listener included. Your route runs with req.body filled in.

  4. Trailing bytes arrive

    The request is still piped into the decoder. Bytes after the compressed data reach the finished decoder, which raises an error. With no listener, Node.js throws and the process exits.

How trailing bytes reach a decoder nobody is listening to

Notice that the pipe from the request into the decoder outlives raw-body's listeners. The late bytes travel down that same pipe after the error listener is gone.

Diagram connections:

  • Client to Request stream (body plus trailing bytes)
  • Request stream to Gunzip decoder (pipe)
  • Gunzip decoder to raw-body reader (decoded bytes, then end)
  • raw-body reader to Your route handler (body parsed)
  • Gunzip decoder to Error on decoder (late bytes)
  • Error on decoder to Process exits (no error listener)
  • Client: Sends a gzip body plus extra bytes
  • Request stream: Carries the body into body-parser
  • Gunzip decoder: req.pipe(decoder) keeps feeding it
  • raw-body reader: Removes its error listener at end
  • Your route handler: Runs with req.body set
  • Error on decoder: Late bytes hit a finished decoder
  • Process exits: Uncaught exception, exit code 1

Who can send it, and to which routes

The advisory says no login and no matching route are needed, because parsing happens before routing. That holds when the parser is mounted with app.use() for every path.

Two limits apply. A parser only reads bodies whose Content-Type it accepts, application/json by default for express.json(). If you mount a parser on one route, only that route reads bodies.

The parser must also accept compressed bodies, and it does by default: inflate is true in both 1.x and 2.x. body-parser 1.x unzips gzip and deflate. 2.x, from 2.0.0, adds br, and 2.4.0 adds zstd where Node.js supports it.

What did body-parser 2.4.0 and 1.20.9 change to stop the crash?

The fix keeps the decoder covered until it is gone. After the body is read, body-parser now detaches the decoder from the request, gives it an empty error listener, destroys it and drains the request.

We confirmed this by downloading the npm tarballs and comparing 2.3.0 with 2.4.0, and 1.20.8 with 1.20.9. The new lines sit in lib/read.js, right after the body is read.

The new lines in lib/read.js, as shipped in body-parser 2.4.0
New lineWhat it doesWhy it matters
req.unpipe(stream)Stops feeding request bytes into the decoderLate bytes no longer reach a finished decoder
stream.on('error', () => {})Adds an empty error listenerAn error during teardown is ignored, not thrown
stream.destroy()Closes the decoder for goodNo half-alive decoder is left behind
req.resume()Reads and drops the rest of the requestThe HTTP request can finish, per the code comment
Charset check moved earlierWhen a verify option is set, rejects an unsupported charset with 415 before any decoder existsIn older versions this 415 was a second way to leave a piped decoder unguarded

1.20.9 does the same work, with a fallback for Node.js 0.8 streams that lack unpipe(dest). Its changelog lists only the security fix. 2.4.0 also adds zstd support and two small performance changes, per its release notes.

The project's new tests send trailing bytes after gzip and deflate bodies with plain http.request calls, so the bytes really arrive after the compressed data. The 1.x fix's tests for this case expect a clean result only on Node.js 11 and later. We did not run them, because they send the crash input.

How do Express apps get body-parser, and why does Express 4.21.2 matter?

Most apps never install body-parser by hand. It arrives as a transitive dependency, a package that another package pulls in, through Express. The version you run depends on Express's version range and on your lockfile.

A range like ^2.3.0 accepts any later 2.x release. A range like ~1.20.5 accepts later 1.20.x releases only. An exact pin like 1.20.3 accepts nothing else.

A lockfile, package-lock.json for npm, records the exact version installed. npm install keeps the locked version while it still fits the range. We saw this with Express 5.3.0 holding body-parser 2.3.0 until we ran an update.

Which body-parser each Express line installs, checked with fresh npm installs on 25 October 2026
Express versionbody-parser rangeFresh install getsHow to reach the fix
5.0.0 to 5.3.0^2.0.1 up to ^2.3.02.4.0 (tested on 5.0.0 and 5.3.0)npm update body-parser if the lockfile holds an older 2.x
4.22.0 to 4.22.3~1.20.3 or ~1.20.51.20.9 (tested on 4.22.0 and 4.22.3)npm update body-parser if the lockfile holds an older 1.20.x
4.16.0 to 4.21.2Exact pin, 1.20.3 in 4.21.2The pinned old version, 1.18.2 on 4.16.0npm update express to 4.22.3, or an npm override
Below 4.16.0No body-parser dependencyNothing from ExpressUpgrade body-parser itself if you installed it

Deciding how to reach a fixed body-parser

Notice that the path depends on who pulls body-parser in. Only the Express 4.21.2 and older branch needs more than an npm update or install of body-parser.

Diagram connections:

  • Run npm ls body-parser to 1.20.9 or 2.4.0 or later?
  • 1.20.9 or 2.4.0 or later? to Patched (Yes)
  • 1.20.9 or 2.4.0 or later? to Who pulls it in? (No)
  • Who pulls it in? to Your own package.json (Direct dependency)
  • Who pulls it in? to Express 5 or 4.22.x (Express with a range)
  • Who pulls it in? to Express 4.21.2 or older (Express with exact pin)
  • Your own package.json to Run npm ls body-parser (check again)
  • Express 5 or 4.22.x to Patched (npm ls shows the fix)
  • Express 4.21.2 or older to Patched (npm ls shows the fix)
  • Run npm ls body-parser: Lists every copy and its parent
  • 1.20.9 or 2.4.0 or later?: Compare each copy's version
  • Who pulls it in?: Read the line above body-parser
  • Your own package.json: npm install [email protected] or @1.20.9
  • Express 5 or 4.22.x: npm update body-parser
  • Express 4.21.2 or older: npm update express, or add an override
  • Patched: Commit package-lock.json and deploy

How do you check whether your app runs a vulnerable body-parser?

Run npm ls body-parser in the project folder, ideally inside the image you deploy. It prints every copy in the dependency tree, with its version and the package that pulled it in.

text
$ npm ls body-parser
[email protected] /app
`-- [email protected]
  `-- [email protected]

This output means the app is exposed, because 1.20.3 is below 1.20.9. Any 1.x below 1.20.9, or any 2.x below 2.4.0, is affected. A large project can show several copies, and each one counts.

npm audit checks the same tree against the GitHub Advisory Database. On our Express 5.3.0 project locked to body-parser 2.3.0, it listed GHSA-27r3-jxmh-p828 as high severity. After the update it printed found 0 vulnerabilities.

On Express 4.21.2, npm audit also listed a second body-parser advisory, GHSA-v422-hmwv-36x6, about the limit option. In our run, body-parser 1.20.9 cleared both, though other Express 4.21.2 packages were still flagged.

How do you upgrade body-parser in Express 4 and Express 5 apps?

Update the package that pulls body-parser in, then confirm with npm ls. Every command below was run on 25 October 2026 with npm 10.9.9.

  1. Find every copy

    Run npm ls body-parser. Note each version and the package above it.

  2. Express 5 or Express 4.22.x

    Run npm update body-parser. It moved our locked 2.3.0 to 2.4.0, the newest version Express 5.3.0's range allows.

  3. Express 4.21.2 or older

    If package.json lists Express as ^4 or similar, run npm update express. Our 4.21.2 project moved to Express 4.22.3 with body-parser 1.20.9.

  4. Express pinned exactly

    If package.json pins an exact old Express, npm update body-parser changes nothing. Raise the Express version, or add the npm override shown below.

  5. body-parser as your own dependency

    Run npm install [email protected] or npm install [email protected], keeping your current major version.

  6. Check and commit

    Run npm ls body-parser again and your tests. Commit package-lock.json so CI and production install the same versions.

An npm override forces a package version that a dependency would not choose. Add this to package.json and run npm install. The nested form pins only the body-parser under Express.

json
{
  "overrides": {
    "express": {
      "body-parser": "1.20.9"
    }
  }
}

Afterwards npm ls body-parser shows both [email protected] and [email protected] marked overridden. We ran Express 4.21.2 with this override, and plain and gzip JSON bodies still parsed with status 200.

A flat override, with body-parser at the top level, would force 1.20.9 on every copy, including any 2.x copy another package needs. An override is also a pin you must remember to remove. Prefer moving to Express 4.22.3, whose own range already allows 1.20.9.

When should you set inflate: false instead of upgrading?

Use inflate: false as a stopgap when you cannot deploy an upgrade today and your clients never send compressed request bodies. body-parser then rejects compressed bodies with a 415 error before it creates any decoder.

We read this in the unpatched 1.20.8 source: the inflate check throws before the decoder is built. Each parser has its own options, so set it on every one you mount.

javascript
app.use(express.json({ inflate: false }))
app.use(express.urlencoded({ extended: false, inflate: false }))

We sent well-formed requests to Express 5.3.0 with the unpatched body-parser 2.3.0, and to the patched versions. The results matched on every version we tried.

text
default, plain JSON  -> 200 {"got":{"city":"Pune"}}
default, gzip JSON   -> 200 {"got":{"city":"Pune"}}
inflate:false, plain -> 200 {"got":{"city":"Pune"}}
inflate:false, gzip  -> 415 {"status":415,"type":"encoding.unsupported"}

The 415 body shape comes from our test's error handler; yours will look different. To know whether clients compress bodies, search your access or proxy logs for requests with a Content-Encoding header.

How do you keep one crash bug from taking the whole service down?

Run several processes, restart them automatically and alert on restarts. The upgrade fixes this bug; these habits limit the damage of the next one.

  • Restart automatically. A process manager such as systemd, PM2 or Kubernetes starts a new process when one exits. The Node.js docs recommend an external monitor in a separate process for this.
  • Run more than one process. Behind a load balancer, one exit drops only that process's in-flight requests. With a single process, every user waits for the restart.
  • Alert on restart counts. One crash may be noise; the same crash every few seconds is an attack or a bad deploy. A quiet restart loop hides the bug.
  • Handle uncaughtException only to log and exit. The Node.js docs say it is not safe to resume normal operation after an uncaught exception.
  • Give every stream you create an error listener for its whole life. stream.pipeline() helps: in our test it passed an error from the last stream to one callback, and the process kept running.

Here is the log-and-exit pattern, run on Node.js 22.23.4. It printed fatal: decoder failed and exited with code 1, so a supervisor sees a failure and restarts it.

javascript
// Last-resort handler: log, then exit non-zero so a supervisor restarts the process.
process.on('uncaughtException', (err) => {
  console.error('fatal:', err.message)
  process.exit(1)
})
const { EventEmitter } = require('node:events')
new EventEmitter().emit('error', new Error('decoder failed'))

Restarts do not stop an attacker who repeats the request. They shorten each outage and buy time for the upgrade, nothing more.

Is the October 2026 undici advisory the same bug?

It is the same kind of bug in a different package, pointing the other way. undici is the HTTP client behind the fetch() function built into Node.js. Its advisory, GHSA-xjj6-f33q-929c, is about responses, not requests.

A malicious server sends a response with more than five content codings and closes the connection before the body. undici rejects the response, but an internal stream it already made has no error listener. The process crashes even if your code catches the fetch() rejection.

Affected undici versions run from 6.23.0, 7.18.0 and 8.0.0 up to the fixes in 6.29.1, 7.30.1 and 8.12.2. Upgrading body-parser does nothing for undici, and the reverse is also true. The undici inside Node.js ships with Node.js itself; process.versions.undici shows its version.

What does the advisory not tell you yet?

The advisory leaves four questions open as of 25 October 2026. They are which encodings trigger it, whether Express will require the fix, which Node.js versions are affected and whether anyone exploits it.

  • Which encodings trigger it. The advisory says such as gzip, and the fix's tests cover gzip and deflate. Treat every encoding your parser accepts as in scope.
  • Whether Express will publish releases that require the fix. Express 5.3.0 and 4.22.3 allow it, but an old lockfile can still hold an old body-parser.
  • Which Node.js versions are affected. The advisory names none, and the 1.x fix's tests expect a clean result only on Node.js 11 and later.
  • Whether anyone is exploiting it. The advisory mentions no attacks seen in the wild. That can change, so do not wait for it.

How do you explain the body parser vulnerability in a system design interview?

Frame it as a lifetime bug in a shared library, then give the fix at three levels: code, dependencies and operations. The steps below are an answer frame you can say in about two minutes.

  1. State the impact

    One request with no login crashes a Node.js process whose body-parser, on default options, reads that request. Availability only, CVSS 7.5.

  2. Explain the mechanism

    The decoder stays piped from the request after the reader removes its error listener. Late bytes cause an error with no listener, so Node.js throws.

  3. Give the code fix

    Detach the decoder, add an error listener, destroy it and drain the request, as body-parser 2.4.0 and 1.20.9 do.

  4. Give the dependency fix

    Find every copy with npm ls, update or override, and commit the lockfile. Mention that Express 4.21.2 pins an exact old version.

  5. Give the operational fix

    Several processes, automatic restarts, alerts on restart loops and inflate: false where compression is not needed.

  6. Name the general lesson

    Any stream that can still receive bytes needs an error listener for as long as it lives.

Questions people ask

Is my app affected if no client ever sends compressed requests?
Yes, if body-parser runs with default options. The sender chooses the Content-Encoding header, not you, and inflate defaults to true. An attacker can send a gzip body even if no real client ever does. Setting inflate: false or upgrading closes that path.
Does this affect apps that do not use Express?
Yes, if they use body-parser directly, for example with a plain Node.js http server or another framework that accepts Express middleware. The bug sits in body-parser's read code, not in Express. Run npm ls body-parser to see whether anything in your tree pulls it in.
Will a try/catch or an Express error handler stop the crash?
No. The decoder emits its error later, outside your route code and outside Express's error middleware. Our test of a try/catch around a delayed error event still exited with code 1. Only an error listener on that stream stops it, and that is what the fixed versions add.
Is a process.on('uncaughtException') handler a safe fix?
No. The Node.js docs call it a crude last resort and say it is not safe to resume normal operation after an uncaught exception. Use the handler to log and exit with a non-zero code, and let a supervisor restart the process. The real fix is upgrading body-parser.
Why did npm update body-parser not change my version?
Most likely you run an Express version from 4.16.0 to 4.21.2, which pins an exact body-parser version. npm will not move a pinned dependency. Update Express to 4.22.3, which allows 1.20.9, or add an npm override. Then run npm ls body-parser to confirm the new version.
Do I need to upgrade Node.js to fix this?
No. The fix is in body-parser, an npm package inside node_modules, and upgrading Node.js does not change it. Keep Node.js current anyway for its own security releases. The undici behind fetch() is one part that comes with Node.js, not from npm.
What does the CVSS score of 7.5 mean here?
It is a high severity score out of 10. The vector says the attack works over the network, needs low effort, no login and no user action. It harms availability only, not confidentiality or integrity. In plain words, attackers can stop your app but cannot read or change your data through this bug.

Sources and further reading

Practise this with Lala

Practise