diff --git a/lib/core/request.js b/lib/core/request.js index 8e2072a46c7..d79526c57ef 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -47,6 +47,8 @@ const kHandler = Symbol('handler') const kController = Symbol('controller') const kResume = Symbol('resume') +let isUndiciFormData + class RequestController { #paused = false #reason = null @@ -206,7 +208,13 @@ class Request { this.body = body.byteLength ? Buffer.from(body) : null } else if (typeof body === 'string') { this.body = body.length ? Buffer.from(body) : null - } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) { + } else if (isFormDataLike(body)) { + isUndiciFormData ??= require('../web/webidl').webidl.is.FormData + if (!isUndiciFormData(body)) { + throw new InvalidArgumentError('FormData body must be an instance of undici\'s FormData class. FormData objects from other realms (e.g. Node.js\' global FormData) are not supported.') + } + this.body = body + } else if (isIterable(body) || isBlobLike(body)) { this.body = body } else { throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') diff --git a/test/issue-5520.js b/test/issue-5520.js new file mode 100644 index 00000000000..b343fac7f3f --- /dev/null +++ b/test/issue-5520.js @@ -0,0 +1,54 @@ +'use strict' + +const { test } = require('node:test') +const { createServer } = require('node:http') +const { createServer: createH2Server } = require('node:http2') +const { once } = require('node:events') +const { request, H2CClient, errors } = require('..') + +// https://github.com/nodejs/undici/issues/5520 + +test('request() rejects a cross-realm (Node builtin) FormData body', { timeout: 60000 }, async (t) => { + const server = createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => res.end('{}')) + }) + t.after(() => { + server.closeAllConnections?.() + server.close() + }) + + server.listen(0) + await once(server, 'listening') + + const form = new globalThis.FormData() + form.append('field', 'value') + form.append('file', new Blob(['file contents'], { type: 'text/plain' }), 'hello.txt') + + await t.assert.rejects( + request(`http://localhost:${server.address().port}/`, { method: 'POST', body: form }), + errors.InvalidArgumentError + ) +}) + +test('HTTP/2 client rejects a cross-realm (Node builtin) FormData body', { timeout: 60000 }, async (t) => { + const server = createH2Server((req, res) => { + req.on('data', () => {}) + req.on('end', () => res.end('{}')) + }) + t.after(() => server.close()) + + server.listen(0) + await once(server, 'listening') + + const client = new H2CClient(`http://localhost:${server.address().port}`) + t.after(() => client.close()) + + const form = new globalThis.FormData() + form.append('field', 'value') + + await t.assert.rejects( + client.request({ path: '/', method: 'POST', body: form }), + errors.InvalidArgumentError + ) +})