Skip to content

Commit

Permalink
feat: add origin check for CSRF protection
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Apr 4, 2024
1 parent 0a407c4 commit 3d56339
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 1 deletion.
12 changes: 12 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,18 @@ export interface AstroUserConfig {
* See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details, including the limitations of this experimental feature.
*/
i18nDomains?: boolean;

/**
* @docs
* @name experimental.csrfProtection
* @type {boolean}
* @default `false`
* @version 4.6.0
* @description
*
* TODO
*/
csrfProtection?: boolean;
};
}

Expand Down
40 changes: 40 additions & 0 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
ComponentInstance,
ManifestData,
MiddlewareHandler,
RouteData,
SSRManifest,
} from '../../@types/astro.js';
Expand Down Expand Up @@ -31,6 +32,7 @@ import { createAssetLink } from '../render/ssr-element.js';
import { ensure404Route } from '../routing/astro-designed-error-pages.js';
import { matchRoute } from '../routing/match.js';
import { AppPipeline } from './pipeline.js';
import { defineMiddleware, sequence } from '../middleware/index.js';
export { deserializeManifest } from './common.js';

export interface RenderOptions {
Expand Down Expand Up @@ -112,6 +114,13 @@ export class App {
* @private
*/
#createPipeline(streaming = false) {
if (this.#manifest.csrfProtection) {
this.#manifest.middleware = sequence(
this.#createOriginCheckMiddleware(),
this.#manifest.middleware
);
}

return AppPipeline.create({
logger: this.#logger,
manifest: this.#manifest,
Expand All @@ -137,6 +146,37 @@ export class App {
});
}

/**
* Content types that can be passed when sending a request via a form
*
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype
* @private
*/
#formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'];

#createOriginCheckMiddleware(): MiddlewareHandler {
return defineMiddleware((context, next) => {
const { request, url } = context;
const contentType = request.headers.get('content-type');
if (contentType) {
if (this.#formContentTypes.includes(contentType)) {
const forbidden =
(request.method === 'POST' ||
request.method === 'PUT' ||
request.method === 'PATCH' ||
request.method === 'DELETE') &&
request.headers.get('origin') !== url.origin;
if (forbidden) {
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
status: 403,
});
}
}
}
return next();
});
}

set setManifestData(newManifestData: ManifestData) {
this.#manifestData = newManifestData;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type SSRManifest = {
pageMap?: Map<ComponentPath, ImportComponentInstance>;
i18n: SSRManifestI18n | undefined;
middleware: MiddlewareHandler;
csrfProtection: SSRCsrfProtection | undefined;
};

export type SSRManifestI18n = {
Expand All @@ -74,6 +75,10 @@ export type SSRManifestI18n = {
domainLookupTable: Record<string, string>;
};

export type SSRCsrfProtection = {
origin: boolean;
};

export type SerializedSSRManifest = Omit<
SSRManifest,
'middleware' | 'routes' | 'assets' | 'componentMetadata' | 'inlinedScripts' | 'clientDirectives'
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { RouteCache } from './render/route-cache.js';
* Thus, a `Pipeline` is created once at process start and then used by every `RenderContext`.
*/
export abstract class Pipeline {
readonly internalMiddleware: MiddlewareHandler[];
protected internalMiddleware: MiddlewareHandler[];

constructor(
readonly logger: Logger,
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,5 +615,8 @@ function createBuildManifest(
i18n: i18nManifest,
buildFormat: settings.config.build.format,
middleware,
csrfProtection: settings.config.experimental.csrfProtection
? settings.config.security?.csrfProtection
: undefined,
};
}
3 changes: 3 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,5 +276,8 @@ function buildManifest(
assets: staticFiles.map(prefixAssetPath),
i18n: i18nManifest,
buildFormat: settings.config.build.format,
csrfProtection: settings.config.experimental.csrfProtection
? settings.config.security?.csrfProtection
: undefined,
};
}
14 changes: 14 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const ASTRO_CONFIG_DEFAULTS = {
clientPrerender: false,
globalRoutePriority: false,
i18nDomains: false,
csrfProtection: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -139,6 +140,15 @@ export const AstroConfigSchema = z.object({
.array(z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }))
.default(ASTRO_CONFIG_DEFAULTS.integrations)
),
security: z
.object({
csrfProtection: z
.object({
origin: z.boolean().default(false),
})
.optional(),
})
.optional(),
build: z
.object({
format: z
Expand Down Expand Up @@ -508,6 +518,10 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.globalRoutePriority),
csrfProtection: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.csrfProtection),
i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains),
})
.strict(
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
componentMetadata: new Map(),
inlinedScripts: new Map(),
i18n: i18nManifest,
csrfProtection: settings.config.experimental.csrfProtection
? settings.config.security?.csrfProtection
: undefined,
middleware(_, next) {
return next();
},
Expand Down

0 comments on commit 3d56339

Please sign in to comment.