Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.0.0 #30

Merged
merged 57 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
ab8a85a
wip
amorey Apr 13, 2024
2024334
wip
amorey Apr 13, 2024
abd842e
svelte integration mvp working
amorey Apr 13, 2024
62cea61
wip
amorey Apr 13, 2024
16633ca
fixed linting issues
amorey Apr 13, 2024
9ec8980
wip
amorey Apr 13, 2024
71dce90
wip
amorey Apr 14, 2024
ace331b
wip
amorey Apr 14, 2024
fcff740
wip
amorey Apr 14, 2024
cc070b9
wip
amorey Apr 14, 2024
e66e118
wip
amorey Apr 14, 2024
72fb256
wip
amorey Apr 14, 2024
d8ea627
wip
amorey Apr 14, 2024
67a6878
wip
amorey Apr 14, 2024
232ce57
wip
amorey Apr 15, 2024
eae5dd6
wip
amorey Apr 15, 2024
c506ae0
wip
amorey Apr 15, 2024
2f41f55
wip
amorey Apr 15, 2024
9f1c8b7
wip
amorey Apr 15, 2024
0c4725e
wip
amorey Apr 15, 2024
0dc86c5
wip
amorey Apr 15, 2024
3fd6bc1
wip
amorey Apr 15, 2024
b301813
wip
amorey Apr 15, 2024
11b4b24
wip
amorey Apr 15, 2024
6b22dca
wip
amorey Apr 15, 2024
30df1b1
wip
amorey Apr 15, 2024
3dc99a1
wip
amorey Apr 15, 2024
850b5bf
wip
amorey Apr 15, 2024
b3c668b
wip
amorey Apr 15, 2024
df010d7
wip
amorey Apr 15, 2024
a0f8c08
wip
amorey Apr 15, 2024
c4253be
wip
amorey Apr 15, 2024
0fd56f7
wip
amorey Apr 16, 2024
d4b139d
wip
amorey Apr 16, 2024
25f3fa4
wip
amorey Apr 16, 2024
e37ca59
wip
amorey Apr 16, 2024
6d3d831
wip
amorey Apr 16, 2024
60dcc27
wip
amorey Apr 16, 2024
a7e5e73
wip
amorey Apr 16, 2024
9d00aef
wip
amorey Apr 16, 2024
50303a8
wip
amorey Apr 16, 2024
21b943c
wip
amorey Apr 16, 2024
a1b9ff9
wip
amorey Apr 16, 2024
336137f
wip
amorey Apr 16, 2024
e3a908f
wip
amorey Apr 16, 2024
60d2a61
wip
amorey Apr 16, 2024
6bfc6d1
wip
amorey Apr 16, 2024
a6e75c7
wip
amorey Apr 16, 2024
693f038
wip
amorey Apr 16, 2024
0da5980
wip
amorey Apr 16, 2024
1a37929
wip
amorey Apr 16, 2024
388200f
wip
amorey Apr 16, 2024
18c0cc2
wip
amorey Apr 16, 2024
d54739c
wip
amorey Apr 17, 2024
8c1c95a
wip
amorey Apr 17, 2024
cc217f5
wip
amorey Apr 17, 2024
035a67b
wip
amorey Apr 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
},
"ignorePatterns": [],
"rules": {
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/extensions": "off",
"import/prefer-default-export": "off",
"max-classes-per-file": "off",
"max-len": "off",
"no-await-in-loop": "off",
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ jobs:
with:
version: 8
- run: pnpm install
- run: pnpm test run
- run: pnpm test run -- --environment node
- run: pnpm test run -- --environment edge-runtime
- run: pnpm build # only necessary for miniflare
- run: pnpm test run -- --environment miniflare

build:
runs-on: ubuntu-latest
Expand Down
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,29 @@
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
.DS_Store
*~
\#*
node_modules
.next
.env
.env*.local
.env.*
!.env.example
dist
build
.vscode

# vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# sveltkit
.svelte-kit

# vercel
.vercel

# wrangler files
.wrangler
.dev.vars
277 changes: 42 additions & 235 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/).

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 helps you to implement the [signed double submit cookie pattern](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#signed-double-submit-cookie-recommended) 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/)). The recommended way to use this library is via its drop-in integrations for [Next.js](src/nextjs) and [SvelteKit](src/sveltekit) though it also has a lower-level API for more custom implementations.

We hope you enjoy using this software. Contributions and suggestions are welcome!

## 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 a Next.js integration ([see here](src/nextjs))
- Includes a SvelteKit integration ([see here](src/sveltekit))
- Includes a low-level API for custom integrations ([see below](#api))
- 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,247 +29,53 @@ pnpm add edge-csrf
yarn add edge-csrf
```

Next, 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';

// initalize protection function
const csrfProtect = csrf({
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;
}
```

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:

### App Router

```typescript
// app/page.tsx

import { headers } from 'next/headers';

export default function Page() {
const csrfToken = headers().get('X-CSRF-Token') || 'missing';

return (
<form action="/api/form-handler" method="post">
<input type="hidden" value={csrfToken}>
<input type="text" name="my-input">
<input type="submit">
</form>
);
}
```

```typescript
// app/form-handler/route.ts

import { NextResponse } from 'next/server';

export async function POST() {
return NextResponse.json({ status: 'success'});
}
```

### Pages Router
## Integrations

```typescript
// pages/form.ts
For details about each integration see:

import type { NextPage, GetServerSideProps } from 'next';
import React from 'react';
* [Next.js README](docs/nextjs.md)
* [SvelteKit README](docs/sveltekit.md)

type Props = {
csrfToken: string;
};
## Low-level API

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
const csrfToken = res.getHeader('x-csrf-token') || 'missing';
return { props: { csrfToken } };
}
The following methods are named exports in the the top-level `edge-csrf` module:

const FormPage: NextPage<Props> = ({ csrfToken }) => {
return (
<form action="/api/form-handler" method="post">
<input type="hidden" value={csrfToken}>
<input type="text" name="my-input">
<input type="submit">
</form>
);
}

export default FormPage;
```
createSecret(length) - Create new secret (cryptographically secure)

```typescript
// pages/api/form-handler.ts
* @param {int} length - Byte length of secret
* @returns {Uint8Array} - The secret

import type { NextApiRequest, NextApiResponse } from 'next';
createToken(secret, saltByteLength) - Create new CSRF token (cryptographically insecure
salt hashed with secret)

type Data = {
status: string
};
* @param {Uint8Array} secret - The secret
* @param {int} saltByteLength - Salt length in number of bytes
* @returns {Promise<Uint8Array>} - A promise returning the token in Uint8Array format

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
// this code won't execute unless CSRF token passes validation
res.status(200).json({ status: 'success' });
}
```
getTokenString(request) - Get the CSRF token from the request

## 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

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)):

```tsx
import { revalidatePath } from 'next/cache';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';

export default function Page() {
const csrfToken = headers().get('X-CSRF-Token') || 'missing';

async function myAction(formData: FormData) {
'use server';
console.log('passed csrf validation');
revalidatePath('/');
redirect('/');
}

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>
);
}
```
* @param {Request} request - The request object
* @returns {Promise<string>} - A promise returning the token in string format

### Non-Form Submission
verifyToken(token, secret) - Verify the CSRF token and secret obtained from the request

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)):
* @param {Uint8Array} token - The CSRF token
* @param {Uint8Array} secret - The CSRF secret
* @returns {Promise<boolean>} - A promise returning result of verification

```tsx
// lib/actions.ts
'use server';
utoa(input) - Encode Uint8Array as base64 string

export async function exampleFn(csrfToken: string, data: { key1: string; key2: string; }) {
console.log(data);
}
* @param {Uint8Array} input - The data to be converted from Uint8Array to base64
* @returns {string} The base64 encoded string

```
atou(input) - Decode base64 string into Uint8Array

```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>
</>
);
}
* @param {string} input - The data to be converted from base64 to Uint8Array
* @returns {Uint8Array} - The Uint8Array representing the input string
```

## Configuration

To configure the CSRF middleware function just pass an object containing your options to the initialization method:

```javascript
const csrfProtect = csrf({
cookie: {
name: '_myCsrfSecret'
},
secretByteLength: 20
});
```

Here are the default configuration values:

```javascript
// default config

{
cookie: {
name: '_csrfSecret',
path: '/',
maxAge: undefined,
domain: '',
secure: true,
httpOnly: true,
sameSite: 'strict'
},
excludePathPrefixes: ['/_next/'],
ignoreMethods: ['GET', 'HEAD', 'OPTIONS'],
saltByteLength: 8,
secretByteLength: 18,
token: {
responseHeader: 'X-CSRF-Token',
value: undefined
}
}
```
__Note__: If you're using these methods you're probably working on a custom framework integration. If so, please consider contributing it back to this project!

## Development

Expand All @@ -284,10 +91,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 All @@ -300,4 +107,4 @@ To build Edge-CSRF for production, run the `build` command:
pnpm build
```

The production files will be located in the `dist/` directory.
The build artifacts will be located in the `dist/` directory.
Loading