Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
amorey committed Apr 15, 2024
1 parent 67a6878 commit 232ce57
Showing 1 changed file with 79 additions and 120 deletions.
199 changes: 79 additions & 120 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
# Edge-CSRF

Edge-CSRF is CSRF protection for [Next.js](https://nextjs.org/) that runs in middleware (edge runtime).
Edge-CSRF is a CSRF protection library that runs on the [edge runtime](https://edge-runtime.vercel.app/) (as well as the node runtime).

This library uses the cookie strategy from [expressjs/csurf](https://github.com/expressjs/csurf) and the crypto logic from [pillarjs/csrf](https://github.com/pillarjs/csrf) except it only uses Next.js edge runtime dependencies so it can be used in [Next.js middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware).
This library implements the [signed double submit cookie pattern](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#signed-double-submit-cookie-recommended) using the crypto logic from [pillarjs/csrf](https://github.com/pillarjs/csrf) except it only uses edge runtime dependencies so it can be used in both node environments and in edge functions (e.g. [Vercel Edge Functions](https://vercel.com/docs/functions/runtimes/edge-runtime), [Cloudflare Page Functions](https://developers.cloudflare.com/pages/functions/)). It comes with easy-to-use integrations for Next.js and SvelteKit plus a lower-level API for more custom implementations.

## Features

- Supports app-router and pages-router Next.js 13 and Next.js 14
- Runs in edge runtime
- Implements cookie strategy from [expressjs/csurf](https://github.com/expressjs/csurf) and the crypto logic from [pillarjs/csrf](https://github.com/pillarjs/csrf)
- Runs on both node and edge runtimes
- Includes Next.js middleware integration (Next.js 13, Next.js 14)
- Includes SvelteKit hooks integration
- Gets token from HTTP request header (`X-CSRF-Token`) or from request body field (`csrf_token`)
- Handles form-urlencoded, multipart/form-data or json-encoded HTTP request bodies
- Supports Server Actions via form and non-form submission
- Customizable cookie options
- TypeScript definitions included

**Note: There's an issue with Next.js middleware in v13.3.X and v13.4.X that prevents edge-csrf from working properly with the pages-router in a dev environment (https://github.com/vercel/next.js/issues/48083, https://github.com/vercel/next.js/issues/48546)**

## Quickstart
## Install

To use Edge-CSRF, first add it as a dependency to your app:
To use Edge-CSRF, just add it as a dependency to your app:

```console
npm install edge-csrf
Expand All @@ -28,35 +29,23 @@ pnpm add edge-csrf
yarn add edge-csrf
```

Next, create a middleware file (`middleware.ts`) for your project and add the Edge-CSRF middleware:
## Next.js

To integrate Edge-CSRF with [Next.js](https://nextjs.org/), create a middleware file (`middleware.ts`) for your project and add the Edge-CSRF middleware:

```typescript
// middleware.ts

import csrf from 'edge-csrf';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createMiddleware } from 'edge-csrf/nextjs';

// initalize protection function
const csrfProtect = csrf({
// initalize csrf protection middleware
const csrfMiddleware = createMiddleware({
cookie: {
secure: process.env.NODE_ENV === 'production',
},
});

export async function middleware(request: NextRequest) {
const response = NextResponse.next();

// csrf protection
const csrfError = await csrfProtect(request, response);

// check result
if (csrfError) {
return new NextResponse('invalid csrf token', { status: 403 });
}

return response;
}
export const middleware = csrfMiddleware;
```

Now, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. To add the CSRF token to your forms, you can fetch it from the `X-CSRF-Token` HTTP response header server-side or client-side. For example:
Expand Down Expand Up @@ -136,115 +125,81 @@ export default function handler(req: NextApiRequest, res: NextApiResponse<Data>)
}
```

## Examples

See more examples in the [examples](examples) directory in this repository:

| Next.js Version | Router | Implementation |
| --------------- | ------ | --------------------------------------------------------------------------------------- |
| 13 | app | [HTML form](examples/next13-approuter-html-submission) |
| 13 | app | [JavaScript (dynamic)](examples/next13-approuter-js-submission-dynamic) |
| 13 | app | [JavaScript (static)](examples/next13-approuter-js-submission-static) |
| 13 | pages | [HTML form](examples/next13-pagesrouter-html-submmission) |
| 14 | app | [HTML form](examples/next14-approuter-html-submission) |
| 14 | app | [JavaScript (dynamic)](examples/next14-approuter-js-submission-dynamic) |
| 14 | app | [JavaScript (static)](examples/next14-approuter-js-submission-static) |
| 14 | app | [Sentry](examples/next14-approuter-sentry) |
| 14 | app | [Server action (form)](examples/next14-approuter-server-action-form-submission) |
| 14 | app | [Server action (non-form)](examples/next14-approuter-server-action-non-form-submission) |
| 14 | pages | [HTML form](examples/next14-pagesrouter-html-submission) |

## Server Actions

Edge-CSRF supports server actions with both form and non-form submission in the latest version of Next.js (14).

### Form Submission
## Sveltekit

With server actions that get executed via form submission, you can add the CSRF token as a hidden field to the form ([see example](examples/next14-approuter-server-action-form-submission)):
To integrate Edge-CSRF with [SvelteKit](https://kit.svelte.dev/), create a server-side hooks file (`hooks.server.ts`) for your project and add the Edge-CSRF handle:

```tsx
import { revalidatePath } from 'next/cache';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
```typescript
// src/hooks.server.ts

export default function Page() {
const csrfToken = headers().get('X-CSRF-Token') || 'missing';
import { createHandle } from 'edge-csrf/sveltekit';

async function myAction(formData: FormData) {
'use server';
console.log('passed csrf validation');
revalidatePath('/');
redirect('/');
}
// initalize csrf protection handle
const csrfHandle = createHandle({
cookie: {
secure: process.env.NODE_ENV === 'production',
},
});

return (
<form action={myAction}>
<legend>Server Action with Form Submission Example:</legend>
<input type="hidden" name="csrf_token" value={csrfToken} />
<input type="text" name="myarg" />
<button type="submit">Submit</button>
</form>
);
}
export const handle = csrfHandle;
```

### Non-Form Submission
Now, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. To add the CSRF token to your forms, you can fetch it from the event's `locals` data object server-side. For example:

With server actions that get executed by JavaScript calls (non-form), you can pass the CSRF token as the first argument to the function ([see example](examples/next14-approuter-server-action-non-form-submission)):

```tsx
// lib/actions.ts
'use server';

export async function exampleFn(csrfToken: string, data: { key1: string; key2: string; }) {
console.log(data);
```typescript
// src/routes/+page.server.ts
export async function load({ locals }) {
return {
csrfToken: locals.csrfToken,
};
}

export const actions = {
default: async () => {
return { success: true };
},
};
```

```tsx
// app/page.tsx
'use client';

import { exampleFn } from '../lib/actions';

export default function Page() {
const handleClick = async () => {
const csrfResp = await fetch('/csrf-token');
const { csrfToken } = await csrfResp.json();

const data = {
key1: 'val1',
key2: 'val2',
};

// use token as first argument to server action
await exampleFn(csrfToken, data);
};

return (
<>
<h2>Server Action with Non-Form Submission Example:</h2>
<button onClick={handleClick}>Click me</button>
</>
);
}
```svelte
<!-- src/routes/+page.svelte -->
<script lang="ts">
export let data;
export let form;
</script>
{#if form?.success}
<span>success</span>
{:else}
<form method="post">
<input type="hidden" value={data.csrfToken}>
<input type="text" name="my-input">
<input type="submit">
</form>
{/if}
```

## Configuration
## Examples

To configure the CSRF middleware function just pass an object containing your options to the initialization method:
See more examples in the [examples](examples) directory in this repository:

```javascript
const csrfProtect = csrf({
cookie: {
name: '_myCsrfSecret'
},
secretByteLength: 20
});
```
| Framework | Implementation |
| ------------------------- | --------------------------------------------------------------------------------------- |
| Next.js 13 (app router) | [HTML form](examples/next13-approuter-html-submission) |
| Next.js 13 (app router) | [JavaScript (dynamic)](examples/next13-approuter-js-submission-dynamic) |
| Next.js 13 (app router) | [JavaScript (static)](examples/next13-approuter-js-submission-static) |
| Next.js 13 (pages router) | [HTML form](examples/next13-pagesrouter-html-submmission) |
| Next.js 14 (app router) | [HTML form](examples/next14-approuter-html-submission) |
| Next.js 14 (app router) | [JavaScript (dynamic)](examples/next14-approuter-js-submission-dynamic) |
| Next.js 14 (app router) | [JavaScript (static)](examples/next14-approuter-js-submission-static) |
| Next.js 14 (app router) | [Sentry](examples/next14-approuter-sentry) |
| Next.js 14 (app router) | [Server action (form)](examples/next14-approuter-server-action-form-submission) |
| Next.js 14 (app router) | [Server action (non-form)](examples/next14-approuter-server-action-non-form-submission) |
| Next.js 14 (pages router) | [HTML form](examples/next14-pagesrouter-html-submission) |
| SvelteKit (vercel) | [HTML form](examples/sveltekit-vercel) |
| SvelteKit (cloudflare) | [HTML form](examples/sveltekit-cloudflare) |

Here are the default configuration values:
## Configuration

```javascript
// default config
Expand All @@ -270,6 +225,10 @@ Here are the default configuration values:
}
```

## API



## Development

### Get the code
Expand All @@ -284,10 +243,10 @@ pnpm install

### Run the unit tests

Edge-CSRF uses jest for testing (via vitest). To run the tests, use the `test` command:
Edge-CSRF uses jest for testing (via vitest). To run the tests in node, edge and miniflare environments, use the `test-all` command:

```console
pnpm test
pnpm test-all
```

The test files are colocated with the source code in the `src/` directory, with the filename format `{name}.test.ts`.
Expand Down

0 comments on commit 232ce57

Please sign in to comment.