diff --git a/src/Formidable.js b/src/Formidable.js index 5ea964e0..ce492c1c 100644 --- a/src/Formidable.js +++ b/src/Formidable.js @@ -6,6 +6,7 @@ import dezalgo from 'dezalgo'; import { EventEmitter } from 'node:events'; import fsPromises from 'node:fs/promises'; import os from 'node:os'; +import stream from 'node:stream'; import path from 'node:path'; import { StringDecoder } from 'node:string_decoder'; import once from 'once'; @@ -201,8 +202,8 @@ class IncomingForm extends EventEmitter { } } const callback = once(dezalgo(cb)); - this.fields = {}; - const files = {}; + this.fields = Object.create(null); + const files = Object.create(null); this.on('field', (name, value) => { if (this.type === 'multipart' || this.type === 'urlencoded') { @@ -274,7 +275,7 @@ class IncomingForm extends EventEmitter { break; default: - pipe = node_stream.Transform({ + pipe = stream.Transform({ transform: function (chunk, encoding, callback) { callback(null, chunk); } diff --git a/src/plugins/json.js b/src/plugins/json.js index ca1e6f24..9503f0ac 100644 --- a/src/plugins/json.js +++ b/src/plugins/json.js @@ -27,7 +27,7 @@ function init(_self, _opts) { const parser = new JSONParser(this.options); parser.on('data', (fields) => { - this.fields = fields; + this.fields = Object.assign(Object.create(null), fields); }); parser.once('end', () => { diff --git a/test-node/standalone/prototype_contamination.test.js b/test-node/standalone/prototype_contamination.test.js new file mode 100644 index 00000000..3efdaf71 --- /dev/null +++ b/test-node/standalone/prototype_contamination.test.js @@ -0,0 +1,107 @@ +import { ok, strictEqual } from 'node:assert'; +import { createServer } from 'node:http'; +import test from 'node:test'; +import formidable, { errors } from '../../src/index.js'; + + + +let server; +let port = 13000; + +test.beforeEach(() => { + // Increment port to avoid conflicts between tests + port += 1; + server = createServer(); +}); + +test('prototype contamination', async (t) => { + server.on('request', async (req, res) => { + const form = formidable(); + + const [fields, files] = await form.parse(req); + + res.writeHead(200); + res.end("ok"); + + let a; + try { + a = typeof String(fields); + } catch { + ; + } + strictEqual(a, undefined, "the toString method should not be used directly"); + + }); + + await new Promise(resolve => server.listen(port, resolve)); + + const body = `{"toString":"x","hasOwnProperty":"x","a":5}`; + + const resClient = await fetch(String(new URL(`http:localhost:${port}/`)), { + method: 'POST', + headers: { + 'Content-Length': body.length, + Host: `localhost:${port}`, + 'Content-Type': 'text/json;', + }, + body + }); + + strictEqual(resClient.status, 200); + + const text = await resClient.text(); + + ok(text); +}); + +test('should not use unsafe methods on user provided objects', async (t) => { + server.on('request', async (req, res) => { + const form = formidable(); + + const [fields, files] = await form.parse(req); + + res.writeHead(200); + res.end("ok"); + + let a; + try { + a = typeof String(fields); + } catch { + ; + } + strictEqual(a, undefined, "the toString method should not be used directly"); + + }); + + await new Promise(resolve => server.listen(port, resolve)); + + const body = `{"a":"x","b":"x","z":5}`; + + const resClient = await fetch(String(new URL(`http:localhost:${port}/`)), { + method: 'POST', + headers: { + 'Content-Length': body.length, + Host: `localhost:${port}`, + 'Content-Type': 'text/json;', + }, + body + }); + + strictEqual(resClient.status, 200); + + const text = await resClient.text(); + + ok(text); +}); + + + +test.afterEach(async () => { + await new Promise((resolve) => { + if (server.listening) { + server.close(() => resolve()); + } else { + resolve(); + } + }); +});