From cd4f4d2932167feda0f9fc3fa5bd33727ea0f9b1 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Wed, 1 Jul 2026 22:57:48 +0100 Subject: [PATCH 1/2] fix: enforce maxFileSize and maxTotalFileSize on octet-stream uploads The octet-stream upload path wrote every chunk to disk without checking the documented maxFileSize/maxTotalFileSize limits, unlike the multipart path in _handlePart. Accumulate per-file and total sizes and abort via _error with the existing FormidableError codes when a cap is exceeded, mirroring the multipart implementation. The over-limit file is removed through the shared _error cleanup, so no partial bytes remain on disk. --- src/plugins/octetstream.js | 27 ++++++++++++++++++++++ test/integration/octet-stream.test.js | 32 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/plugins/octetstream.js b/src/plugins/octetstream.js index 8a0b2709..2fed2eb5 100644 --- a/src/plugins/octetstream.js +++ b/src/plugins/octetstream.js @@ -1,6 +1,8 @@ /* eslint-disable no-underscore-dangle */ import OctetStreamParser from "../parsers/OctetStream.js"; +import * as errors from "../FormidableError.js"; +import FormidableError from "../FormidableError.js"; export const octetStreamType = "octet-stream"; // the `options` is also available through the `options` / `formidable.options` @@ -47,8 +49,33 @@ async function init(_self, _opts) { // Keep track of writes that haven't finished so we don't emit the file before it's done being written let outstandingWrites = 0; + let fileSize = 0; this._parser.on("data", (buffer) => { + fileSize += buffer.length; + this._totalFileSize += buffer.length; + + if (fileSize > this.options.maxFileSize) { + this._error( + new FormidableError( + `options.maxFileSize (${this.options.maxFileSize} bytes), received ${fileSize} bytes of file data`, + errors.biggerThanMaxFileSize, + 413 + ) + ); + return; + } + if (this._totalFileSize > this.options.maxTotalFileSize) { + this._error( + new FormidableError( + `options.maxTotalFileSize (${this.options.maxTotalFileSize} bytes) exceeded, received ${this._totalFileSize} bytes of file data`, + errors.biggerThanTotalMaxFileSize, + 413 + ) + ); + return; + } + this.pause(); outstandingWrites += 1; diff --git a/test/integration/octet-stream.test.js b/test/integration/octet-stream.test.js index b6b6ab11..bc30743e 100644 --- a/test/integration/octet-stream.test.js +++ b/test/integration/octet-stream.test.js @@ -52,3 +52,35 @@ test("octet stream", (done) => { createReadStream(testFilePath).pipe(request); }); }); + +test("octet stream enforces maxFileSize", (done) => { + const PORT2 = PORT + 1; + const server = createServer((req, res) => { + const form = formidable({ maxFileSize: 1024, maxTotalFileSize: 2048 }); + + form.parse(req, (err, fields, files) => { + // a 256KB octet-stream body must be rejected, not written to disk + assert(err, "expected an error for over-sized octet-stream upload"); + strictEqual(err.code, 1016); // biggerThanMaxFileSize + strictEqual(Object.keys(files).length, 0); + + res.end(); + server.close(); + done(); + }); + }); + + server.listen(PORT2, (err) => { + assert(!err, "should not have error, but be falsey"); + + const request = _request({ + port: PORT2, + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + }, + }); + + request.end(Buffer.alloc(256 * 1024, 0x42)); + }); +}); From 1a43a6198488331ec9661a8ab03d9ad1a5ef82ce Mon Sep 17 00:00:00 2001 From: Yarchik Date: Fri, 25 Sep 2026 16:32:38 +0100 Subject: [PATCH 2/2] test(octet-stream): close the rejected upload's connection The maxFileSize test left a TCPWRAP handle open: formidable aborts the parse mid-body, so the connection never completes on its own and server.close() only stops new ones. Jest reported "did not exit one second after the test run" locally and "A worker process has failed to exit gracefully" on CI, where the leaked socket's late callback surfaced as a failure inside the next test file, test/integration/store-files-option.test.js. Destroying the client request ends the connection. Measured: with the old teardown --detectOpenHandles reports 1 open TCPWRAP at the server.listen line; with this change it reports none, and the full suite is 15/15 suites, 95 passed / 3 skipped of 98, with no worker warning. --- test/integration/octet-stream.test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/integration/octet-stream.test.js b/test/integration/octet-stream.test.js index bc30743e..61f56540 100644 --- a/test/integration/octet-stream.test.js +++ b/test/integration/octet-stream.test.js @@ -55,6 +55,7 @@ test("octet stream", (done) => { test("octet stream enforces maxFileSize", (done) => { const PORT2 = PORT + 1; + let request; const server = createServer((req, res) => { const form = formidable({ maxFileSize: 1024, maxTotalFileSize: 2048 }); @@ -65,6 +66,9 @@ test("octet stream enforces maxFileSize", (done) => { strictEqual(Object.keys(files).length, 0); res.end(); + // The upload was rejected mid-body, so the connection never completes on + // its own; without this the socket outlives the test and Jest's worker. + request.destroy(); server.close(); done(); }); @@ -73,7 +77,7 @@ test("octet stream enforces maxFileSize", (done) => { server.listen(PORT2, (err) => { assert(!err, "should not have error, but be falsey"); - const request = _request({ + request = _request({ port: PORT2, method: "POST", headers: { @@ -81,6 +85,8 @@ test("octet stream enforces maxFileSize", (done) => { }, }); + // Destroying the request above surfaces here as ECONNRESET. + request.on("error", () => {}); request.end(Buffer.alloc(256 * 1024, 0x42)); }); });