diff --git a/.changeset/chatty-experts-smell.md b/.changeset/chatty-experts-smell.md new file mode 100644 index 000000000000..c9ac56f37482 --- /dev/null +++ b/.changeset/chatty-experts-smell.md @@ -0,0 +1,30 @@ +--- +"astro": minor +--- + +The CSRF protection feature that was introduced behind a flag in [v4.6.0](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md#460) is no longer experimental and is available for general use. + +To enable the stable version, add the new top-level `security` option in `astro.config.mjs`. If you were previously using the experimental version of this feature, also delete the experimental flag: + +```diff +export default defineConfig({ +- experimental: { +- security: { +- csrfProtection: { +- origin: true +- } +- } +- }, ++ security: { ++ checkOrigin: true ++ } +}) +``` + +Enabling this setting performs a check that the `"origin"` header, automatically passed by all modern browsers, matches the URL sent by each Request. + +This check is executed only for pages rendered on demand, and only for the requests `POST`, `PATCH`, `DELETE` and `PUT` with one of the following `"content-type"` headers: `'application/x-www-form-urlencoded'`, `'multipart/form-data'`, `'text/plain'`. + +If the `"origin"` header doesn't match the pathname of the request, Astro will return a 403 status code and won't render the page. + +For more information, see the [`security` configuration docs](https://docs.astro.build/en/reference/configuration-reference/#security). diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 82f4977a2076..b1782f39c326 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -779,6 +779,47 @@ export interface AstroUserConfig { */ scopedStyleStrategy?: 'where' | 'class' | 'attribute'; + /** + * @docs + * @name security + * @type {boolean} + * @default `{}` + * @version 4.9.0 + * @description + * + * Enables security measures for an Astro website. + * + * These features only exist for pages rendered on demand (SSR) using `server` mode or pages that opt out of prerendering in `hybrid` mode. + * + * ```js + * // astro.config.mjs + * export default defineConfig({ + * output: "server", + * security: { + * checkOrigin: true + * } + * }) + * ``` + */ + security?: { + /** + * @name security.checkOrigin + * @type {boolean} + * @default 'false' + * @version 4.6.0 + * @description + * + * When enabled, performs a check that the "origin" header, automatically passed by all modern browsers, matches the URL sent by each `Request`. This is used to provide Cross-Site Request Forgery (CSRF) protection. + * + * The "origin" check is executed only for pages rendered on demand, and only for the requests `POST, `PATCH`, `DELETE` and `PUT` with + * the following `content-type` header: 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'. + * + * If the "origin" header doesn't match the `pathname` of the request, Astro will return a 403 status code and will not render the page. + */ + + checkOrigin?: boolean; + }; + /** * @docs * @name vite @@ -1955,63 +1996,7 @@ export interface AstroUserConfig { * In the event of route collisions, where two routes of equal route priority attempt to build the same URL, Astro will log a warning identifying the conflicting routes. */ globalRoutePriority?: boolean; - - /** - * @docs - * @name experimental.security - * @type {boolean} - * @default `false` - * @version 4.6.0 - * @description - * - * Enables CSRF protection for Astro websites. - * - * The CSRF protection works only for pages rendered on demand (SSR) using `server` or `hybrid` mode. The pages must opt out of prerendering in `hybrid` mode. - * - * ```js - * // astro.config.mjs - * export default defineConfig({ - * output: "server", - * experimental: { - * security: { - * csrfProtection: { - * origin: true - * } - * } - * } - * }) - * ``` - */ - security?: { - /** - * @name security.csrfProtection - * @type {object} - * @default '{}' - * @version 4.6.0 - * @description - * - * Allows you to enable security measures to prevent CSRF attacks: https://owasp.org/www-community/attacks/csrf - */ - - csrfProtection?: { - /** - * @name security.csrfProtection.origin - * @type {boolean} - * @default 'false' - * @version 4.6.0 - * @description - * - * When enabled, performs a check that the "origin" header, automatically passed by all modern browsers, matches the URL sent by each `Request`. - * - * The "origin" check is executed only for pages rendered on demand, and only for the requests `POST, `PATCH`, `DELETE` and `PUT` with - * the following `content-type` header: 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'. - * - * If the "origin" header doesn't match the `pathname` of the request, Astro will return a 403 status code and will not render the page. - */ - origin?: boolean; - }; - }; - + /** * @docs * @name experimental.rewriting diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index ad38779487e3..eda7739da58a 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -558,6 +558,6 @@ function createBuildManifest( buildFormat: settings.config.build.format, middleware, rewritingEnabled: settings.config.experimental.rewriting, - checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, + checkOrigin: settings.config.security?.checkOrigin ?? false, }; } diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 561e8875b83c..31c3593dd155 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -277,7 +277,7 @@ function buildManifest( assets: staticFiles.map(prefixAssetPath), i18n: i18nManifest, buildFormat: settings.config.build.format, - checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, + checkOrigin: settings.config.security?.checkOrigin ?? false, rewritingEnabled: settings.config.experimental.rewriting, }; } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 81d5600ea584..ca795b91da3b 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -79,6 +79,7 @@ const ASTRO_CONFIG_DEFAULTS = { vite: {}, legacy: {}, redirects: {}, + security: {}, experimental: { actions: false, directRenderScript: false, @@ -86,7 +87,6 @@ const ASTRO_CONFIG_DEFAULTS = { contentCollectionJsonSchema: false, clientPrerender: false, globalRoutePriority: false, - security: {}, rewriting: false, }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -492,6 +492,12 @@ export const AstroConfigSchema = z.object({ } }) ), + security: z + .object({ + checkOrigin: z.boolean().default(false), + }) + .optional() + .default(ASTRO_CONFIG_DEFAULTS.security), experimental: z .object({ actions: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.actions), @@ -515,17 +521,6 @@ export const AstroConfigSchema = z.object({ .boolean() .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.globalRoutePriority), - security: z - .object({ - csrfProtection: z - .object({ - origin: z.boolean().default(false), - }) - .optional() - .default({}), - }) - .optional() - .default(ASTRO_CONFIG_DEFAULTS.experimental.security), rewriting: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rewriting), }) .strict( diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index f576602458a3..8d65940bf8eb 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -144,7 +144,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest componentMetadata: new Map(), inlinedScripts: new Map(), i18n: i18nManifest, - checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, + checkOrigin: settings.config.security?.checkOrigin ?? false, rewritingEnabled: settings.config.experimental.rewriting, middleware(_, next) { return next(); diff --git a/packages/astro/test/fixtures/csrf-check-origin/astro.config.mjs b/packages/astro/test/fixtures/csrf-check-origin/astro.config.mjs index af516bcd9736..da3e09912556 100644 --- a/packages/astro/test/fixtures/csrf-check-origin/astro.config.mjs +++ b/packages/astro/test/fixtures/csrf-check-origin/astro.config.mjs @@ -3,12 +3,8 @@ import { defineConfig } from 'astro/config'; // https://astro.build/config export default defineConfig({ output: "server", - experimental: { - security: { - csrfProtection: { - origin: true - } - } + security: { + checkOrigin: true } });