diff --git a/.changeset/young-cougars-mix.md b/.changeset/young-cougars-mix.md new file mode 100644 index 000000000000..dd63efd953ef --- /dev/null +++ b/.changeset/young-cougars-mix.md @@ -0,0 +1,6 @@ +--- +'@astrojs/node': patch +'astro': patch +--- + +Make the body request limit a configurable diff --git a/packages/astro/src/actions/runtime/server.ts b/packages/astro/src/actions/runtime/server.ts index 7cca3aeac17a..417101f4aeab 100644 --- a/packages/astro/src/actions/runtime/server.ts +++ b/packages/astro/src/actions/runtime/server.ts @@ -323,9 +323,10 @@ export function getActionContext(context: APIContext): AstroActionContext { throw error; } + const bodySizeLimit = pipeline.manifest.actionBodySizeLimit; let input; try { - input = await parseRequestBody(context.request); + input = await parseRequestBody(context.request, bodySizeLimit); } catch (e) { if (e instanceof ActionError) { return { data: undefined, error: e }; @@ -381,24 +382,22 @@ function getCallerInfo(ctx: APIContext) { return undefined; } -const DEFAULT_ACTION_BODY_SIZE_LIMIT = 1024 * 1024; - -async function parseRequestBody(request: Request) { +async function parseRequestBody(request: Request, bodySizeLimit: number) { const contentType = request.headers.get('content-type'); const contentLengthHeader = request.headers.get('content-length'); const contentLength = contentLengthHeader ? Number.parseInt(contentLengthHeader, 10) : undefined; const hasContentLength = typeof contentLength === 'number' && Number.isFinite(contentLength); if (!contentType) return undefined; - if (hasContentLength && contentLength > DEFAULT_ACTION_BODY_SIZE_LIMIT) { + if (hasContentLength && contentLength > bodySizeLimit) { throw new ActionError({ code: 'CONTENT_TOO_LARGE', - message: `Request body exceeds ${DEFAULT_ACTION_BODY_SIZE_LIMIT} bytes`, + message: `Request body exceeds ${bodySizeLimit} bytes`, }); } if (hasContentType(contentType, formContentTypes)) { if (!hasContentLength) { - const body = await readRequestBodyWithLimit(request.clone(), DEFAULT_ACTION_BODY_SIZE_LIMIT); + const body = await readRequestBodyWithLimit(request.clone(), bodySizeLimit); const formRequest = new Request(request.url, { method: request.method, headers: request.headers, @@ -411,7 +410,7 @@ async function parseRequestBody(request: Request) { if (hasContentType(contentType, ['application/json'])) { if (contentLength === 0) return undefined; if (!hasContentLength) { - const body = await readRequestBodyWithLimit(request.clone(), DEFAULT_ACTION_BODY_SIZE_LIMIT); + const body = await readRequestBodyWithLimit(request.clone(), bodySizeLimit); if (body.byteLength === 0) return undefined; return JSON.parse(new TextDecoder().decode(body)); } diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index 13b8c942e95f..0c010a0c2017 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -162,6 +162,7 @@ function createManifest( i18n: manifest?.i18n, checkOrigin: false, allowedDomains: manifest?.allowedDomains ?? [], + actionBodySizeLimit: 1024 * 1024, middleware: manifest?.middleware ?? middlewareInstance, key: createKey(), csp: manifest?.csp, diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 219d377c543f..2686dc75014c 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -87,6 +87,7 @@ export type SSRManifest = { actions?: () => Promise | SSRActions; checkOrigin: boolean; allowedDomains?: Partial[]; + actionBodySizeLimit: number; sessionConfig?: ResolvedSessionConfig; cacheDir: string | URL; srcDir: string | URL; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 1b586c94ed27..47bb3790283c 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -775,6 +775,7 @@ async function createBuildManifest( actions: () => actions, checkOrigin: (settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false, + actionBodySizeLimit: settings.config.security.actionBodySizeLimit, key, csp, }; diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 8d5d6974acc7..0612522fb9be 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -388,6 +388,7 @@ async function buildManifest( checkOrigin: (settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false, allowedDomains: settings.config.security?.allowedDomains, + actionBodySizeLimit: settings.config.security.actionBodySizeLimit, serverIslandNameMap: Array.from(settings.serverIslandNameMap), key: encodedKey, sessionConfig: settings.config.session, diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index d5b5c6837558..8f8255431406 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -91,6 +91,7 @@ export const ASTRO_CONFIG_DEFAULTS = { security: { checkOrigin: true, allowedDomains: [], + actionBodySizeLimit: 1024 * 1024, }, env: { schema: {}, @@ -440,6 +441,10 @@ export const AstroConfigSchema = z.object({ ) .optional() .default(ASTRO_CONFIG_DEFAULTS.security.allowedDomains), + actionBodySizeLimit: z + .number() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.security.actionBodySizeLimit), }) .optional() .default(ASTRO_CONFIG_DEFAULTS.security), diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index d08fe59006a7..31c1a2f5a218 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -633,6 +633,31 @@ export interface AstroUserConfig< * When not configured, `X-Forwarded-Host` headers are not trusted and will be ignored. */ allowedDomains?: Partial[]; + + /** + * @docs + * @name security.actionBodySizeLimit + * @kind h4 + * @type {number} + * @default `1048576` (1 MB) + * @version 5.x.0 + * @description + * + * Sets the maximum size in bytes allowed for action request bodies. + * + * By default, action request bodies are limited to 1 MB (1048576 bytes) to prevent abuse. + * You can increase this limit if your actions need to accept larger payloads, for example when handling file uploads. + * + * ```js + * // astro.config.mjs + * export default defineConfig({ + * security: { + * actionBodySizeLimit: 10 * 1024 * 1024 // 10 MB + * } + * }) + * ``` + */ + actionBodySizeLimit?: number; }; /** diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index f474e874f076..d1549c8c9a4d 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -130,6 +130,8 @@ export default function createVitePluginAstroServer({ warnMissingAdapter(logger, settings); pipeline.manifest.checkOrigin = settings.config.security.checkOrigin && settings.buildOutput === 'server'; + pipeline.manifest.actionBodySizeLimit = + settings.config.security.actionBodySizeLimit; pipeline.setManifestData(routesList); } @@ -311,6 +313,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest i18n: i18nManifest, checkOrigin: (settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false, + actionBodySizeLimit: settings.config.security.actionBodySizeLimit, key: hasEnvironmentKey() ? getEnvironmentKey() : createKey(), middleware() { return {