Skip to content

fix: enforce maxFileSize/maxTotalFileSize on octet-stream uploads#1113

Open
spokodev wants to merge 1 commit into
node-formidable:masterfrom
spokodev:fix/octetstream-size-limits
Open

fix: enforce maxFileSize/maxTotalFileSize on octet-stream uploads#1113
spokodev wants to merge 1 commit into
node-formidable:masterfrom
spokodev:fix/octetstream-size-limits

Conversation

@spokodev

@spokodev spokodev commented Jul 1, 2026

Copy link
Copy Markdown

The octet-stream upload path does not enforce the documented maxFileSize / maxTotalFileSize limits, unlike the multipart path.

Steps to reproduce

POST a 256KB body with Content-Type: application/octet-stream to a form configured with maxFileSize: 1024:

const form = formidable({ maxFileSize: 1024, maxTotalFileSize: 2048 });
form.parse(req, (err, fields, files) => {
  console.log(err, Object.keys(files).length);
});
// request.end(Buffer.alloc(256 * 1024, 0x42));

ACTUAL: err is undefined, one file is returned with size 262144, and the over-sized file is committed to disk via file.end().

EXPECTED: err.code === 1016 (biggerThanMaxFileSize), no file returned, and nothing left on disk. This matches how the multipart path already behaves.

Root cause

The octet-stream plugin's _parser.on("data", ...) handler in src/plugins/octetstream.js writes each chunk straight to the file with no size check, and it bypasses _handlePart() in src/Formidable.js where the multipart size caps are enforced. As a result a raw octet-stream body of any size is accepted regardless of the configured limits.

The README documents maxFileSize and maxTotalFileSize as limiting each file and the batch respectively, with defaults, and does not exempt octet-stream.

Fix

Accumulate the per-file and running total sizes before each write and abort via this._error(...) with the existing FormidableError codes biggerThanMaxFileSize / biggerThanTotalMaxFileSize when a cap is exceeded, mirroring _handlePart. In-limit uploads are unaffected. Because the octet-stream file is tracked in openedFiles, the shared _error cleanup calls file.destroy(), which unlinks the partial file, so no bytes remain on disk (the same cleanup the multipart path relies on).

Authority

CWE-770 (allocation without limits), plus the library's own documented contract and its multipart implementation, which enforces exactly these caps.

Tests

Added an integration case in test/integration/octet-stream.test.js: a 256KB octet-stream body with maxFileSize: 1024 must be rejected with code 1016 and return no files. Verified it fails on the current source (the over-sized upload is accepted with err null) and passes with the fix, with the tmp directory left empty afterwards.

Suite status: 92 passed / 3 skipped across 14 jest suites, and 11/11 node tests.

Greptile Summary

This PR enforces maxFileSize and maxTotalFileSize on application/octet-stream uploads, closing a gap where those documented limits were silently ignored on the octet-stream path while the multipart path already respected them.

  • src/plugins/octetstream.js: Adds per-chunk size accounting (fileSize, this._totalFileSize) and calls this._error() with the existing biggerThanMaxFileSize / biggerThanTotalMaxFileSize error codes when a limit is exceeded, causing _error() to destroy and unlink the partial file via the shared openedFiles cleanup.
  • test/integration/octet-stream.test.js: Adds an integration test sending a 256 KB body against maxFileSize: 1024, asserting error code 1016 and an empty files object. The maxTotalFileSize branch (error code 1009) is not yet covered by a test.

Confidence Score: 4/5

Safe to merge; the core enforcement logic is correct and the cleanup path (file.destroy → unlink) is reused from the existing multipart implementation.

The fix is small and correctly wired: size counters are accumulated before each write, errors short-circuit via the established _error() path, and endfn's existing this.error guard prevents _parser.end() from firing so the end-handler race is not a concern. The only gaps are a cosmetic duplicate import and a missing test case for the maxTotalFileSize branch, neither of which affects the correctness of the shipped behaviour.

The new biggerThanTotalMaxFileSize branch in src/plugins/octetstream.js (lines 68-77) is not covered by any test.

Important Files Changed

Filename Overview
src/plugins/octetstream.js Adds per-chunk maxFileSize / maxTotalFileSize guards to the data handler, mirroring _handlePart. Logic is correct; imports can be consolidated. The end handler is safe because endfn guards parser.end() on error.
test/integration/octet-stream.test.js Adds an integration test that proves maxFileSize (error code 1016) is enforced. The maxTotalFileSize path (error code 1009) is not covered: the test sets maxTotalFileSize: 2048, but because maxFileSize: 1024 fires first the 1009 branch is never reached.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Formidable
    participant OctetStreamPlugin
    participant PersistentFile

    Client->>Formidable: POST /upload (Content-Type: application/octet-stream)
    Formidable->>OctetStreamPlugin: init() — open file, push to openedFiles
    OctetStreamPlugin->>PersistentFile: file.open()

    loop Per data chunk
        Client->>Formidable: chunk
        Formidable->>OctetStreamPlugin: _parser.emit("data", buffer)
        OctetStreamPlugin->>OctetStreamPlugin: "fileSize += buffer.length"
        OctetStreamPlugin->>OctetStreamPlugin: "_totalFileSize += buffer.length"

        alt "fileSize > maxFileSize"
            OctetStreamPlugin->>Formidable: _error(biggerThanMaxFileSize 1016)
            Formidable->>PersistentFile: file.destroy() → unlink
            Formidable-->>Client: "callback(err, {}, {})"
        else "_totalFileSize > maxTotalFileSize"
            OctetStreamPlugin->>Formidable: _error(biggerThanTotalMaxFileSize 1009)
            Formidable->>PersistentFile: file.destroy() → unlink
            Formidable-->>Client: "callback(err, {}, {})"
        else within limits
            OctetStreamPlugin->>PersistentFile: file.write(buffer)
        end
    end

    alt No error
        OctetStreamPlugin->>PersistentFile: file.end()
        Formidable-->>Client: "callback(null, {}, { file: [...] })"
    else Error already set
        Formidable->>Formidable: endfn checks this.error → returns (parser.end skipped)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Formidable
    participant OctetStreamPlugin
    participant PersistentFile

    Client->>Formidable: POST /upload (Content-Type: application/octet-stream)
    Formidable->>OctetStreamPlugin: init() — open file, push to openedFiles
    OctetStreamPlugin->>PersistentFile: file.open()

    loop Per data chunk
        Client->>Formidable: chunk
        Formidable->>OctetStreamPlugin: _parser.emit("data", buffer)
        OctetStreamPlugin->>OctetStreamPlugin: "fileSize += buffer.length"
        OctetStreamPlugin->>OctetStreamPlugin: "_totalFileSize += buffer.length"

        alt "fileSize > maxFileSize"
            OctetStreamPlugin->>Formidable: _error(biggerThanMaxFileSize 1016)
            Formidable->>PersistentFile: file.destroy() → unlink
            Formidable-->>Client: "callback(err, {}, {})"
        else "_totalFileSize > maxTotalFileSize"
            OctetStreamPlugin->>Formidable: _error(biggerThanTotalMaxFileSize 1009)
            Formidable->>PersistentFile: file.destroy() → unlink
            Formidable-->>Client: "callback(err, {}, {})"
        else within limits
            OctetStreamPlugin->>PersistentFile: file.write(buffer)
        end
    end

    alt No error
        OctetStreamPlugin->>PersistentFile: file.end()
        Formidable-->>Client: "callback(null, {}, { file: [...] })"
    else Error already set
        Formidable->>Formidable: endfn checks this.error → returns (parser.end skipped)
    end
Loading

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix: enforce maxFileSize and maxTotalFil..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

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.
Comment on lines +4 to +5
import * as errors from "../FormidableError.js";
import FormidableError from "../FormidableError.js";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The two separate import statements can be merged into a single combined import, which is the pattern used throughout the codebase (e.g. import FormidableError, * as errors from "./FormidableError.js" in Formidable.js).

Suggested change
import * as errors from "../FormidableError.js";
import FormidableError from "../FormidableError.js";
import FormidableError, * as errors from "../FormidableError.js";

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

Comment on lines +56 to +86
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));
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 maxTotalFileSize branch is never exercised

The test sets maxFileSize: 1024 and maxTotalFileSize: 2048, then sends 256 KB. Because fileSize > maxFileSize fires first (on the very first chunk), the _totalFileSize > maxTotalFileSize branch in octetstream.js lines 68-77 is never reached. The new error code 1009 (biggerThanTotalMaxFileSize) path has zero test coverage. A second case — e.g. maxFileSize: 4096, maxTotalFileSize: 1024 with a 2 KB body — would exercise it and guard against a future regression there.

Fix in Codex Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant