diff --git a/.changeset/odd-ligers-swim.md b/.changeset/odd-ligers-swim.md new file mode 100644 index 000000000000..18f2206a3fda --- /dev/null +++ b/.changeset/odd-ligers-swim.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-vercel': patch +'@sveltejs/kit': patch +--- + +ensure `content-length` limit respected; handle `getRawBody` error(s) diff --git a/packages/adapter-node/src/server.js b/packages/adapter-node/src/server.js index 0f94fa32b3e6..266dd10df1e0 100644 --- a/packages/adapter-node/src/server.js +++ b/packages/adapter-node/src/server.js @@ -40,12 +40,22 @@ export function createServer({ render }) { prerendered_handler, async (req, res) => { const parsed = new URL(req.url || '', 'http://localhost'); + + let body; + + try { + body = await getRawBody(req); + } catch (err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } + const rendered = await render({ method: req.method, headers: req.headers, // TODO: what about repeated headers, i.e. string[] path: parsed.pathname, - rawBody: await getRawBody(req), - query: parsed.searchParams + query: parsed.searchParams, + rawBody: body }); if (rendered) { diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index d0dd9f29d0ae..4fa041c47fee 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -7,12 +7,21 @@ import { render } from '../output/server/app.js'; // eslint-disable-line import/ export default async (req, res) => { const { pathname, searchParams } = new URL(req.url || '', 'http://localhost'); + let body; + + try { + body = await getRawBody(req); + } catch (err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } + const rendered = await render({ method: req.method, headers: req.headers, path: pathname, query: searchParams, - rawBody: await getRawBody(req) + rawBody: body }); if (rendered) { diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index 349673bfe358..9885b30d0e7c 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -153,7 +153,14 @@ class Watcher extends EventEmitter { const root = (await this.vite.ssrLoadModule(`/${this.dir}/generated/root.svelte`)) .default; - const rawBody = await getRawBody(req); + let body; + + try { + body = await getRawBody(req); + } catch (err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } const host = /** @type {string} */ (this.config.kit.host || req.headers[this.config.kit.hostHeader || 'host']); @@ -165,7 +172,7 @@ class Watcher extends EventEmitter { host, path: parsed.pathname, query: new URLSearchParams(parsed.query), - rawBody + rawBody: body }, { amp: this.config.kit.amp, diff --git a/packages/kit/src/core/node/index.js b/packages/kit/src/core/node/index.js index 1f01280eb936..66f03aa53998 100644 --- a/packages/kit/src/core/node/index.js +++ b/packages/kit/src/core/node/index.js @@ -1,44 +1,45 @@ /** * @param {import('http').IncomingMessage} req - * @returns {Promise} + * @returns {Promise} */ export function getRawBody(req) { return new Promise((fulfil, reject) => { const h = req.headers; if (!h['content-type']) { - fulfil(null); - return; + return fulfil(null); } req.on('error', reject); const length = Number(h['content-length']); - /** @type {Uint8Array} */ - let data; - - if (!isNaN(length)) { - data = new Uint8Array(length); + // https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95 + if (isNaN(length) && h['transfer-encoding'] == null) { + return fulfil(null); + } - let i = 0; + let data = new Uint8Array(length || 0); + if (length > 0) { + let offset = 0; req.on('data', (chunk) => { - data.set(chunk, i); - i += chunk.length; - }); - } else { - // https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95 - if (h['transfer-encoding'] === undefined) { - fulfil(null); - return; - } + const new_len = offset + Buffer.byteLength(chunk); - data = new Uint8Array(0); + if (new_len > length) { + return reject({ + status: 413, + reason: 'Exceeded "Content-Length" limit' + }); + } + data.set(chunk, offset); + offset = new_len; + }); + } else { req.on('data', (chunk) => { const new_data = new Uint8Array(data.length + chunk.length); - new_data.set(data); + new_data.set(data, 0); new_data.set(chunk, data.length); data = new_data; }); @@ -48,11 +49,11 @@ export function getRawBody(req) { const [type] = h['content-type'].split(/;\s*/); if (type === 'application/octet-stream') { - fulfil(data); + return fulfil(data); } - const decoder = new TextDecoder(h['content-encoding'] || 'utf-8'); - fulfil(decoder.decode(data)); + const encoding = h['content-encoding'] || 'utf-8'; + fulfil(new TextDecoder(encoding).decode(data)); }); }); } diff --git a/packages/kit/src/core/start/index.js b/packages/kit/src/core/start/index.js index 10b3a134a813..e0682cdd8043 100644 --- a/packages/kit/src/core/start/index.js +++ b/packages/kit/src/core/start/index.js @@ -53,14 +53,23 @@ export async function start({ port, host, config, https: use_https = false, cwd assets_handler(req, res, () => { static_handler(req, res, async () => { + let body; + + try { + body = await getRawBody(req); + } catch (err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } + const rendered = await app.render({ host: /** @type {string} */ (config.kit.host || req.headers[config.kit.hostHeader || 'host']), method: req.method, headers: /** @type {import('types/helper').Headers} */ (req.headers), path: decodeURIComponent(parsed.pathname), - rawBody: await getRawBody(req), - query: new URLSearchParams(parsed.query || '') + query: new URLSearchParams(parsed.query || ''), + rawBody: body }); if (rendered) { diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index e2b91e6b7dbe..5e3875492611 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -5,14 +5,14 @@ export type StrictBody = string | Uint8Array; export type Incoming = Omit & { method: string; headers: Headers; - rawBody: StrictBody; + rawBody: StrictBody | null; body?: ParameterizedBody; }; export type ServerRequest, Body = unknown> = Location & { method: string; headers: Headers; - rawBody: StrictBody; + rawBody: StrictBody | null; body: ParameterizedBody; locals: Locals; };