CVE-2018-3711
Description
Fastify node module before 0.38.0 is vulnerable to a denial-of-service attack by sending a request with "Content-Type: application/json" and a very large payload.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Fastify before 0.38.0 is vulnerable to denial of service via a large JSON payload with Content-Type: application/json.
Vulnerability
Fastify versions before 0.38.0 (npm package) are vulnerable to a denial-of-service (DoS) attack [1]. The vulnerability exists in the default body parser for JSON content when a request with Content-Type: application/json includes an excessively large payload. The parser does not enforce a size limit, causing resource exhaustion [1].
Exploitation
An attacker can send a crafted HTTP POST, PUT, or PATCH request with Content-Type: application/json and a very large body. No authentication or special network position is required; the attacker only needs to reach the vulnerable Fastify server [1][4].
Impact
Successful exploitation leads to a denial of service, making the Fastify server unresponsive or crashing it, affecting availability [1][4].
Mitigation
Fixed in Fastify version 0.38.0 [2][3]. The fix introduces a jsonBodyLimit option to limit request body size [2][3]. Upgrade to 0.38.0 or later. If upgrading is not possible, consider using a reverse proxy to limit request sizes [4].
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
fastifynpm | < 0.38.0 | 0.38.0 |
Affected products
2- HackerOne/fastify node modulev5Range: Versions before 0.38.0
Patches
1fabd2a011f2fAdd option to limit the size of request bodies for the default body parser
7 files changed · +238 −14
docs/Routes.md+1 −0 modified@@ -25,6 +25,7 @@ They need to be in * `beforeHandler(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#before-handler) called just before the request handler, useful if you need to perform authentication at route level for example, it could also be and array of functions. * `handler(request, reply)`: the function that will handle this request. * `schemaCompiler(schema)`: the function that build the schema for the validations. See [here](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-compiler) +* `jsonBodyLimit`: prevents the default JSON body parser from parsing request bodies larger than this number of bytes. Must be an integer. You may also set this option globally when first creating the Fastify instance with `fastify(options)`. Defaults to `1000000` (1 MB). `request` is defined in [Request](https://github.com/fastify/fastify/blob/master/docs/Request.md).
fastify.js+24 −2 modified@@ -92,6 +92,16 @@ function build (options) { server.on('clientError', handleClientError) } + // JSON body limit option + if (options.jsonBodyLimit !== undefined) { + if (!Number.isInteger(options.jsonBodyLimit)) { + throw new TypeError(`'jsonBodyLimit' option must be an integer. Got: '${options.jsonBodyLimit}'`) + } + fastify._jsonBodyLimit = options.jsonBodyLimit + } else { + fastify._jsonBodyLimit = 1000 * 1000 // 1 MB + } + // shorthand methods fastify.delete = _delete fastify.get = _get @@ -370,7 +380,8 @@ function build (options) { schema: options.schema, beforeHandler: options.beforeHandler, config: options.config, - schemaCompiler: options.schemaCompiler + schemaCompiler: options.schemaCompiler, + jsonBodyLimit: options.jsonBodyLimit }) } @@ -394,6 +405,14 @@ function build (options) { throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`) } + var jsonBodyLimit = _fastify._jsonBodyLimit + if (opts.jsonBodyLimit !== undefined) { + if (!Number.isInteger(opts.jsonBodyLimit)) { + throw new TypeError(`'jsonBodyLimit' option must be an integer. Got: '${opts.jsonBodyLimit}'`) + } + jsonBodyLimit = opts.jsonBodyLimit + } + _fastify.after((notHandledErr, done) => { const path = opts.url || opts.path const prefix = _fastify._routePrefix @@ -411,6 +430,7 @@ function build (options) { config, _fastify._errorHandler, _fastify._middie, + jsonBodyLimit, _fastify ) @@ -463,7 +483,7 @@ function build (options) { return _fastify } - function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, middie, fastify) { + function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, middie, jsonBodyLimit, fastify) { this.schema = schema this.handler = handler this.Reply = Reply @@ -476,6 +496,7 @@ function build (options) { this.config = config this.errorHandler = errorHandler this._middie = middie + this._jsonBodyLimit = jsonBodyLimit this._fastify = fastify } @@ -614,6 +635,7 @@ function build (options) { opts.config || {}, this._errorHandler, this._middie, + this._jsonBodyLimit, null )
lib/handleRequest.js+51 −10 modified@@ -23,7 +23,7 @@ function handleRequest (req, res, params, context) { if (method === 'POST' || method === 'PUT' || method === 'PATCH') { // application/json content type if (contentType && contentType.indexOf('application/json') > -1) { - return jsonBody(request, reply) + return jsonBody(request, reply, context._jsonBodyLimit) } // custom parser for a given content type @@ -42,7 +42,7 @@ function handleRequest (req, res, params, context) { // application/json content type if (contentType.indexOf('application/json') > -1) { - return jsonBody(request, reply) + return jsonBody(request, reply, context._jsonBodyLimit) } // custom parser for a given content type if (context.contentTypeParser.fastHasHeader(contentType)) { @@ -57,19 +57,60 @@ function handleRequest (req, res, params, context) { return } -function jsonBody (request, reply) { - var body = '' - var req = request.req - req.on('error', onError) +function jsonBody (request, reply, limit) { + const contentLength = Number.parseInt(request.headers['content-length'], 10) + if (contentLength > limit) { + reply.code(413).send(new Error('Request body is too large')) + return + } + + const req = request.req + const chunks = [] + var receivedLength = 0 + req.on('data', onData) req.on('end', onEnd) - function onError (err) { - reply.code(422).send(err) + req.on('error', onEnd) + + function removeHandlers () { + req.removeListener('data', onData) + req.removeListener('end', onEnd) + req.removeListener('error', onEnd) } + function onData (chunk) { - body += chunk + receivedLength += chunk.length + + if (receivedLength > limit) { + removeHandlers() + reply.code(413).send(new Error('Request body is too large')) + return + } + + chunks.push(chunk) } - function onEnd () { + + function onEnd (err) { + removeHandlers() + + if (err !== undefined) { + reply.code(400).send(err) + return + } + + if (!Number.isNaN(contentLength) && receivedLength !== contentLength) { + reply.code(400).send(new Error('Request body size did not match Content-Length')) + return + } + + if (receivedLength === 0) { // Body is invalid JSON + reply.code(422).send(new Error('Unexpected end of JSON input')) + return + } + + const body = chunks.length === 1 + ? chunks[0].toString() + : chunks.join('') try { request.body = JSON.parse(body) } catch (err) {
test/fastify-options.test.js+46 −0 added@@ -0,0 +1,46 @@ +'use strict' + +const Fastify = require('..') +const sget = require('simple-get').concat +const t = require('tap') +const test = t.test + +test('jsonBodyLimit option', t => { + t.plan(5) + + try { + Fastify({ jsonBodyLimit: 1.3 }) + t.fail('option must be an integer') + } catch (err) { + t.ok(err) + } + + try { + Fastify({ jsonBodyLimit: [] }) + t.fail('option must be an integer') + } catch (err) { + t.ok(err) + } + + const fastify = Fastify({ jsonBodyLimit: 1 }) + + fastify.post('/', (request, reply) => { + reply.send({error: 'handler should not be called'}) + }) + + fastify.listen(0, function (err) { + t.error(err) + fastify.server.unref() + + sget({ + method: 'POST', + url: 'http://localhost:' + fastify.server.address().port, + headers: { 'Content-Type': 'application/json' }, + body: [], + json: true + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 413) + }) + }) +})
test/helper.js+93 −1 modified@@ -1,6 +1,7 @@ 'use strict' const sget = require('simple-get').concat +const stream = require('stream') module.exports.payloadMethod = function (method, t) { const test = t.test @@ -60,6 +61,18 @@ module.exports.payloadMethod = function (method, t) { } }) + test(`${upMethod} with jsonBodyLimit option`, t => { + t.plan(1) + try { + fastify[loMethod]('/with-limit', { jsonBodyLimit: 1 }, function (req, reply) { + reply.send(req.body) + }) + t.pass() + } catch (e) { + t.fail() + } + }) + fastify.listen(0, function (err) { if (err) { t.error(err) @@ -83,6 +96,22 @@ module.exports.payloadMethod = function (method, t) { }) }) + test(`${upMethod} - correctly replies with very large body`, t => { + t.plan(3) + + const largeString = 'world'.repeat(13200) + sget({ + method: upMethod, + url: 'http://localhost:' + fastify.server.address().port, + body: { hello: largeString }, + json: true + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + t.deepEqual(body, { hello: largeString }) + }) + }) + test(`${upMethod} - correctly replies if the content type has the charset`, t => { t.plan(3) sget({ @@ -167,7 +196,8 @@ module.exports.payloadMethod = function (method, t) { } test(`${upMethod} returns 422 - Unprocessable Entity`, t => { - t.plan(2) + t.plan(4) + sget({ method: upMethod, url: 'http://localhost:' + fastify.server.address().port, @@ -180,6 +210,68 @@ module.exports.payloadMethod = function (method, t) { t.error(err) t.strictEqual(response.statusCode, 422) }) + + sget({ + method: upMethod, + url: 'http://localhost:' + fastify.server.address().port, + body: '', + headers: { 'Content-Type': 'application/json' }, + timeout: 500 + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 422) + }) + }) + + test(`${upMethod} returns 413 - Payload Too Large`, t => { + t.plan(6) + + sget({ + method: upMethod, + url: 'http://localhost:' + fastify.server.address().port, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': '1000001' + } + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 413) + }) + + var chunk = Buffer.allocUnsafe(1000 * 1000 + 1) + const largeStream = new stream.Readable({ + read () { + this.push(chunk) + chunk = null + } + }) + sget({ + method: upMethod, + url: 'http://localhost:' + fastify.server.address().port, + headers: { 'Content-Type': 'application/json' }, + body: largeStream, + timeout: 500 + }, (err, response, body) => { + t.error(err) + if (upMethod === 'OPTIONS') { + // Node errors with a 400 Bad Request for OPTIONS requests + // with a stream body and no Content-Length header + t.strictEqual(response.statusCode, 400) + } else { + t.strictEqual(response.statusCode, 413) + } + }) + + sget({ + method: upMethod, + url: `http://localhost:${fastify.server.address().port}/with-limit`, + headers: { 'Content-Type': 'application/json' }, + body: {}, + json: true + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 413) + }) }) }) }
test/internals/handleRequest.test.js+1 −1 modified@@ -105,7 +105,7 @@ test('jsonBody should be a function', t => { t.plan(2) t.is(typeof internals.jsonBody, 'function') - t.is(internals.jsonBody.length, 2) + t.is(internals.jsonBody.length, 3) }) test('request should be defined in onSend Hook on post request with content type application/json', t => {
test/route.test.js+22 −0 modified@@ -178,3 +178,25 @@ test('path can be specified in place of uri', t => { t.deepEqual(JSON.parse(res.payload), { hello: 'world' }) }) }) + +test('invalid jsonBodyLimit option - route', t => { + t.plan(2) + + try { + fastify.route({ + jsonBodyLimit: false, + method: 'PUT', + handler: () => null + }) + t.fail('jsonBodyLimit must be an integer') + } catch (err) { + t.ok(err) + } + + try { + fastify.post('/url', { jsonBodyLimit: 10000.1 }, () => null) + t.fail('jsonBodyLimit must be an integer') + } catch (err) { + t.ok(err) + } +})
Vulnerability mechanics
Root cause
"The default JSON body parser lacked a mechanism to limit the size of incoming request bodies, allowing for memory exhaustion via large payloads."
Attack vector
An attacker can trigger a denial-of-service by sending a request with the `Content-Type: application/json` header and an extremely large payload. Because the server would attempt to buffer the entire request body into memory without a limit, this could lead to resource exhaustion. [patch_id=14206]
Affected code
The vulnerability exists in `lib/handleRequest.js` within the `jsonBody` function. This function was responsible for parsing incoming JSON request bodies without enforcing any size constraints on the incoming data stream. [patch_id=14206]
What the fix does
The patch introduces a `jsonBodyLimit` option, which defaults to 1 MB, to restrict the size of incoming JSON request bodies. The `jsonBody` function in `lib/handleRequest.js` was updated to check the `Content-Length` header and monitor the incoming data stream against this limit. If the limit is exceeded, the server now terminates the request and returns a 413 Payload Too Large status code. [patch_id=14206]
Preconditions
- configThe application must be using a version of Fastify prior to 0.38.0.
- networkThe attacker must be able to send HTTP requests to the Fastify server.
Generated on May 17, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-mq6c-fh97-4gwvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-3711ghsaADVISORY
- github.com/fastify/fastify/commit/fabd2a011f2ffbb877394abe699f549513ffbd76ghsaWEB
- github.com/fastify/fastify/pull/627ghsax_refsource_MISCWEB
- hackerone.com/reports/303632ghsax_refsource_MISCWEB
- www.npmjs.com/advisories/564ghsaWEB
News mentions
0No linked articles in our index yet.