Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 816bdc4

Browse files
authored
feat(netlify): verification for edge middleware (#152)
* feat(netlify): verification for edge middleware * add changeset * remove unused folder
1 parent 7a5c3b0 commit 816bdc4

File tree

4 files changed

+25
-4
lines changed

4 files changed

+25
-4
lines changed

.changeset/odd-shoes-talk.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@astrojs/netlify': minor
3+
---
4+
5+
Implements verification for edge middleware. This is a security measure to ensure that your serverless functions are only ever called by your edge middleware and not by a third party.
6+
7+
When `edgeMiddleware` is enabled, the serverless function will now respond with `403 Forbidden` for requests that are not verified to have come from the generated edge middleware. No user action is necessary.

packages/netlify/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { AstroConfig, AstroIntegration, RouteData } from 'astro';
66
import { AstroError } from 'astro/errors';
77
import { build } from 'esbuild';
88
import { appendFile, mkdir, readFile, rm, writeFile } from 'fs/promises';
9+
import type { Args } from "./ssr-function.js"
910

1011
const { version: packageVersion } = JSON.parse(
1112
await readFile(new URL('../package.json', import.meta.url), 'utf8')
@@ -75,6 +76,8 @@ export default function netlifyIntegration(
7576
let outDir: URL;
7677
let rootDir: URL;
7778
let astroMiddlewareEntryPoint: URL | undefined = undefined;
79+
// Secret used to verify that the caller is the astro-generated edge middleware and not a third-party
80+
const middlewareSecret = crypto.randomUUID();
7881

7982
const ssrOutputDir = () => new URL('./.netlify/functions-internal/ssr/', rootDir);
8083
const middlewareOutputDir = () => new URL('.netlify/edge-functions/middleware/', rootDir);
@@ -136,6 +139,7 @@ export default function netlifyIntegration(
136139
const next = () => {
137140
const { netlify, ...otherLocals } = ctx.locals;
138141
request.headers.set("x-astro-locals", trySerializeLocals(otherLocals));
142+
request.headers.set("x-astro-middleware-secret", "${middlewareSecret}");
139143
return context.next();
140144
};
141145
@@ -270,15 +274,18 @@ export default function netlifyIntegration(
270274
'See https://github.com/withastro/adapters/tree/main/packages/netlify#image-cdn for more.'
271275
);
272276
}
277+
278+
const edgeMiddleware = integrationConfig?.edgeMiddleware ?? false;
273279

274280
setAdapter({
275281
name: '@astrojs/netlify',
276282
serverEntrypoint: '@astrojs/netlify/ssr-function.js',
277283
exports: ['default'],
278284
adapterFeatures: {
279285
functionPerRoute: false,
280-
edgeMiddleware: integrationConfig?.edgeMiddleware ?? false,
286+
edgeMiddleware,
281287
},
288+
args: { middlewareSecret } satisfies Args,
282289
supportedAstroFeatures: {
283290
hybridOutput: 'stable',
284291
staticOutput: 'stable',

packages/netlify/src/ssr-function.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { applyPolyfills } from 'astro/app/node';
66
applyPolyfills();
77

88
// biome-ignore lint/complexity/noBannedTypes: safe to use in this case
9-
export type Args = {};
9+
export interface Args {
10+
middlewareSecret: string;
11+
};
1012

1113
const clientAddressSymbol = Symbol.for('astro.clientAddress');
1214

13-
export const createExports = (manifest: SSRManifest, _args: Args) => {
15+
export const createExports = (manifest: SSRManifest, { middlewareSecret }: Args) => {
1416
const app = new App(manifest);
1517

1618
function createHandler(integrationConfig: {
@@ -27,7 +29,13 @@ export const createExports = (manifest: SSRManifest, _args: Args) => {
2729
let locals: Record<string, unknown> = {};
2830

2931
const astroLocalsHeader = request.headers.get('x-astro-locals');
32+
const middlewareSecretHeader = request.headers.get('x-astro-middleware-secret');
3033
if (astroLocalsHeader) {
34+
if (middlewareSecretHeader !== middlewareSecret) {
35+
return new Response("Forbidden", { status: 403 })
36+
}
37+
// hide the secret from the rest of user and library code
38+
request.headers.delete('x-astro-middleware-secret');
3139
locals = JSON.parse(astroLocalsHeader);
3240
}
3341

packages/netlify/test/functions/fixtures/split-support/src/env.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)