diff --git a/.changeset/young-cougars-mix.md b/.changeset/young-cougars-mix.md new file mode 100644 index 000000000000..7136b8c2a766 --- /dev/null +++ b/.changeset/young-cougars-mix.md @@ -0,0 +1,19 @@ +--- +'@astrojs/node': patch +'astro': patch +--- + +Adds a new `security.actionBodySizeLimit` option to configure the maximum size of Astro Actions request bodies. + +This lets you increase the default 1 MB limit when your actions need to accept larger payloads. For example, actions that handle file uploads or large JSON payloads can now opt in to a higher limit. + +If you do not set this option, Astro continues to enforce the 1 MB default to help prevent abuse. + +```js +// astro.config.mjs +export default defineConfig({ + security: { + actionBodySizeLimit: 10 * 1024 * 1024 // set to 10 MB + } +}) +``` diff --git a/packages/astro/src/actions/runtime/server.ts b/packages/astro/src/actions/runtime/server.ts index d015466121cb..5daeaf803dcc 100644 --- a/packages/astro/src/actions/runtime/server.ts +++ b/packages/astro/src/actions/runtime/server.ts @@ -195,9 +195,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 }; @@ -253,24 +254,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, @@ -283,7 +282,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 6a887277d035..c4f671187a15 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -164,6 +164,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 48c0e008653e..f5a372f4cbf9 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -107,6 +107,7 @@ export type SSRManifest = { sessionDriver?: () => Promise<{ default: SessionDriverFactory | null }>; checkOrigin: boolean; allowedDomains?: Partial[]; + actionBodySizeLimit: number; sessionConfig?: SSRManifestSession; cacheDir: URL; srcDir: URL; diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index fc5b05c4213a..695e6acfd884 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -336,6 +336,10 @@ async function buildManifest( buildFormat: settings.config.build.format, checkOrigin: (settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false, + actionBodySizeLimit: + settings.config.security?.actionBodySizeLimit && settings.buildOutput === 'server' + ? settings.config.security.actionBodySizeLimit + : 1024 * 1024, allowedDomains: settings.config.security?.allowedDomains, key: encodedKey, sessionConfig: sessionConfigToManifest(settings.config.session), diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index 477acacea1f0..c5c1d9a1ddae 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -93,6 +93,7 @@ export const ASTRO_CONFIG_DEFAULTS = { checkOrigin: true, allowedDomains: [], csp: false, + actionBodySizeLimit: 1024 * 1024, }, env: { schema: {}, @@ -439,6 +440,10 @@ export const AstroConfigSchema = z.object({ ) .optional() .default(ASTRO_CONFIG_DEFAULTS.security.allowedDomains), + actionBodySizeLimit: z + .number() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.security.actionBodySizeLimit), csp: z .union([ z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.security.csp), diff --git a/packages/astro/src/manifest/serialized.ts b/packages/astro/src/manifest/serialized.ts index f2b5e3e5ca0b..0096813f167d 100644 --- a/packages/astro/src/manifest/serialized.ts +++ b/packages/astro/src/manifest/serialized.ts @@ -167,6 +167,8 @@ async function createSerializedManifest(settings: AstroSettings): Promise[]; + /** + * @docs + * @name security.actionBodySizeLimit + * @kind h4 + * @type {number} + * @default `1048576` (1 MB) + * @version 5.18.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; + /** * @docs * @name security.csp diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 3d8f4b009d5f..2c9f37288876 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -192,6 +192,8 @@ export async function createDevelopmentManifest(settings: AstroSettings): Promis i18n: i18nManifest, checkOrigin: (settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false, + actionBodySizeLimit: + (settings.config.security?.actionBodySizeLimit) ? settings.config.security.actionBodySizeLimit : 1024 * 1024, // 1mb default key: hasEnvironmentKey() ? getEnvironmentKey() : createKey(), middleware() { return {