summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Arsh <69170106+lilnasy@users.noreply.github.com> 2024-02-07 16:09:39 +0000
committerGravatar GitHub <noreply@github.com> 2024-02-07 21:39:39 +0530
commit0699f34d5c4481c027c4d29d73944f79f97008df (patch)
tree8515ffb1d2194fb60eb0e816ee034e3a03e97895
parent9ef79173a6a826a7ee20682c6925a18e6ec839d3 (diff)
downloadastro-0699f34d5c4481c027c4d29d73944f79f97008df.tar.gz
astro-0699f34d5c4481c027c4d29d73944f79f97008df.tar.zst
astro-0699f34d5c4481c027c4d29d73944f79f97008df.zip
feat(vercel): middleware verification (#9987)
* feat(vercel): verification for edge middleware * add changeset * Apply suggestions from code review --------- Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
-rw-r--r--.changeset/slimy-zebras-march.md7
-rw-r--r--packages/integrations/vercel/src/serverless/adapter.ts19
-rw-r--r--packages/integrations/vercel/src/serverless/entrypoint.ts32
-rw-r--r--packages/integrations/vercel/src/serverless/middleware.ts17
4 files changed, 60 insertions, 15 deletions
diff --git a/.changeset/slimy-zebras-march.md b/.changeset/slimy-zebras-march.md
new file mode 100644
index 000000000..a3f0bcc65
--- /dev/null
+++ b/.changeset/slimy-zebras-march.md
@@ -0,0 +1,7 @@
+---
+"@astrojs/vercel": minor
+---
+
+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 a third party.
+
+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.
diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts
index 36924c508..0281b62d4 100644
--- a/packages/integrations/vercel/src/serverless/adapter.ts
+++ b/packages/integrations/vercel/src/serverless/adapter.ts
@@ -42,6 +42,7 @@ export const ASTRO_PATH_PARAM = 'x_astro_path';
* with the locals serialized into this header.
*/
export const ASTRO_LOCALS_HEADER = 'x-astro-locals';
+export const ASTRO_MIDDLEWARE_SECRET_HEADER = 'x-astro-middleware-secret';
export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware';
// Vercel routes the folder names to a path on the deployed website.
@@ -67,14 +68,17 @@ const SUPPORTED_NODE_VERSIONS: Record<
function getAdapter({
edgeMiddleware,
functionPerRoute,
+ middlewareSecret,
}: {
edgeMiddleware: boolean;
functionPerRoute: boolean;
+ middlewareSecret: string;
}): AstroAdapter {
return {
name: PACKAGE_NAME,
serverEntrypoint: `${PACKAGE_NAME}/entrypoint`,
exports: ['default'],
+ args: { middlewareSecret },
adapterFeatures: {
edgeMiddleware,
functionPerRoute,
@@ -190,6 +194,8 @@ export default function vercelServerless({
let _middlewareEntryPoint: URL | undefined;
// Extra files to be merged with `includeFiles` during build
const extraFilesToInclude: URL[] = [];
+ // Secret used to verify that the caller is the astro-generated edge middleware and not a third-party
+ const middlewareSecret = crypto.randomUUID();
return {
name: PACKAGE_NAME,
@@ -248,7 +254,7 @@ export default function vercelServerless({
);
}
- setAdapter(getAdapter({ functionPerRoute, edgeMiddleware }));
+ setAdapter(getAdapter({ functionPerRoute, edgeMiddleware, middlewareSecret }));
_config = config;
_buildTempFolder = config.build.server;
@@ -356,7 +362,11 @@ export default function vercelServerless({
}
}
if (_middlewareEntryPoint) {
- await builder.buildMiddlewareFolder(_middlewareEntryPoint, MIDDLEWARE_PATH);
+ await builder.buildMiddlewareFolder(
+ _middlewareEntryPoint,
+ MIDDLEWARE_PATH,
+ middlewareSecret
+ );
}
const fourOhFourRoute = routes.find((route) => route.pathname === '/404');
// Output configuration
@@ -472,13 +482,14 @@ class VercelBuilder {
});
}
- async buildMiddlewareFolder(entry: URL, functionName: string) {
+ async buildMiddlewareFolder(entry: URL, functionName: string, middlewareSecret: string) {
const functionFolder = new URL(`./functions/${functionName}.func/`, this.config.outDir);
await generateEdgeMiddleware(
entry,
new URL(VERCEL_EDGE_MIDDLEWARE_FILE, this.config.srcDir),
- new URL('./middleware.mjs', functionFolder)
+ new URL('./middleware.mjs', functionFolder),
+ middlewareSecret
);
await writeJson(new URL(`./.vc-config.json`, functionFolder), {
diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts
index a60f03d7a..5dfba7697 100644
--- a/packages/integrations/vercel/src/serverless/entrypoint.ts
+++ b/packages/integrations/vercel/src/serverless/entrypoint.ts
@@ -1,26 +1,44 @@
import type { SSRManifest } from 'astro';
import { applyPolyfills, NodeApp } from 'astro/app/node';
import type { IncomingMessage, ServerResponse } from 'node:http';
-import { ASTRO_PATH_HEADER, ASTRO_PATH_PARAM, ASTRO_LOCALS_HEADER } from './adapter.js';
+import {
+ ASTRO_PATH_HEADER,
+ ASTRO_PATH_PARAM,
+ ASTRO_LOCALS_HEADER,
+ ASTRO_MIDDLEWARE_SECRET_HEADER,
+} from './adapter.js';
applyPolyfills();
-export const createExports = (manifest: SSRManifest) => {
+export const createExports = (
+ manifest: SSRManifest,
+ { middlewareSecret }: { middlewareSecret: string }
+) => {
const app = new NodeApp(manifest);
const handler = async (req: IncomingMessage, res: ServerResponse) => {
const url = new URL(`https://example.com${req.url}`);
const clientAddress = req.headers['x-forwarded-for'] as string | undefined;
const localsHeader = req.headers[ASTRO_LOCALS_HEADER];
+ const middlewareSecretHeader = req.headers[ASTRO_MIDDLEWARE_SECRET_HEADER];
const realPath = req.headers[ASTRO_PATH_HEADER] ?? url.searchParams.get(ASTRO_PATH_PARAM);
if (typeof realPath === 'string') {
req.url = realPath;
}
- const locals =
- typeof localsHeader === 'string'
+
+ let locals = {};
+ if (localsHeader) {
+ if (middlewareSecretHeader !== middlewareSecret) {
+ res.statusCode = 403;
+ res.end('Forbidden');
+ return;
+ }
+ locals = typeof localsHeader === 'string'
? JSON.parse(localsHeader)
- : Array.isArray(localsHeader)
- ? JSON.parse(localsHeader[0])
- : {};
+ : JSON.parse(localsHeader[0]);
+ }
+ // hide the secret from the rest of user code
+ delete req.headers[ASTRO_MIDDLEWARE_SECRET_HEADER];
+
const webResponse = await app.render(req, { addCookieHeader: true, clientAddress, locals });
await NodeApp.writeResponse(webResponse, res);
};
diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts
index 648bc0d68..91d032873 100644
--- a/packages/integrations/vercel/src/serverless/middleware.ts
+++ b/packages/integrations/vercel/src/serverless/middleware.ts
@@ -1,7 +1,12 @@
import { existsSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { builtinModules } from 'node:module';
-import { ASTRO_LOCALS_HEADER, ASTRO_PATH_HEADER, NODE_PATH } from './adapter.js';
+import {
+ ASTRO_MIDDLEWARE_SECRET_HEADER,
+ ASTRO_LOCALS_HEADER,
+ ASTRO_PATH_HEADER,
+ NODE_PATH,
+} from './adapter.js';
/**
* It generates the Vercel Edge Middleware file.
@@ -17,11 +22,13 @@ import { ASTRO_LOCALS_HEADER, ASTRO_PATH_HEADER, NODE_PATH } from './adapter.js'
export async function generateEdgeMiddleware(
astroMiddlewareEntryPointPath: URL,
vercelEdgeMiddlewareHandlerPath: URL,
- outPath: URL
+ outPath: URL,
+ middlewareSecret: string
): Promise<URL> {
const code = edgeMiddlewareTemplate(
astroMiddlewareEntryPointPath,
- vercelEdgeMiddlewareHandlerPath
+ vercelEdgeMiddlewareHandlerPath,
+ middlewareSecret
);
// https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware
const bundledFilePath = fileURLToPath(outPath);
@@ -56,7 +63,8 @@ export async function generateEdgeMiddleware(
function edgeMiddlewareTemplate(
astroMiddlewareEntryPointPath: URL,
- vercelEdgeMiddlewareHandlerPath: URL
+ vercelEdgeMiddlewareHandlerPath: URL,
+ middlewareSecret: string
) {
const middlewarePath = JSON.stringify(
fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/')
@@ -85,6 +93,7 @@ export default async function middleware(request, context) {
fetch(new URL('${NODE_PATH}', request.url), {
headers: {
...Object.fromEntries(request.headers.entries()),
+ '${ASTRO_MIDDLEWARE_SECRET_HEADER}': '${middlewareSecret}',
'${ASTRO_PATH_HEADER}': request.url.replace(origin, ''),
'${ASTRO_LOCALS_HEADER}': trySerializeLocals(ctx.locals)
}