Giter Club home page Giter Club logo

edge-csrf's Introduction

Edge-CSRF

Edge-CSRF is a CSRF protection library for JavaScript that runs on the edge runtime.

The Edge-CSRF library helps you to implement the signed double submit cookie pattern 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, Cloudflare Page Functions). The recommended way to use this library is via its drop-in integrations for Next.js and 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

  • Runs on both node and edge runtimes
  • Includes a Next.js integration (see here)
  • Includes a SvelteKit integration (see here)
  • Includes a low-level API for custom integrations (see here)
  • Handles form-urlencoded, multipart/form-data or json-encoded HTTP request bodies
  • Gets token from HTTP request header or from request body
  • Supports Server Actions via form and non-form submission
  • Customizable cookie and other options

Integrations

Quickstart (Next.js)

First, install Edge-CSRF's Next.js integration library:

npm install @edge-csrf/nextjs
# or
pnpm add @edge-edge-csrf/nextjs
# or
yarn add @edge-csrf/nextjs

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

// middleware.ts

import { createCsrfMiddleware } from '@edge-csrf/nextjs';

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

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:

// 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>
  );
}
// app/form-handler/route.ts

import { NextResponse } from 'next/server';

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

Quickstart (SvelteKit)

First, install Edge-CSRF's SvelteKit integration library:

npm install @edge-csrf/sveltekit
# or
pnpm add @edge-edge-csrf/sveltekit
# or
yarn add @edge-csrf/sveltekit

Next, create a server-side hooks file (hooks.server.ts) for your project and add the Edge-CSRF handle:

// src/hooks.server.ts

import { createCsrfHandle } from '@edge-csrf/sveltekit';

// initalize csrf protection handle
const csrfHandle = createCsrfHandle({
  cookie: {
    secure: process.env.NODE_ENV === 'production',
  },
});

export const handle = csrfHandle;

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:

// src/routes/+page.server.ts

export async function load({ locals }) {
  return {
    csrfToken: locals.csrfToken,
  };
}

export const actions = {
  default: async () => {
    return { success: true };
  },
};
<!-- 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}

Finally, to make typescript aware of the new locals attributes you can add Edge-CSRF types to your app's types:

// src/app.d.ts

import type { CsrfLocals } from '@edge-csrf/sveltekit';

declare global {
  namespace App {
    // ...
    interface Locals extends CsrfLocals {}
    // ...
  }
}

export {};

Development

Get the code

To develop edge-csrf, first clone the repository then install the dependencies:

git clone [email protected]:kubetail-org/edge-csrf.git
cd edge-csrf
pnpm install

Run the unit tests

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

pnpm test-all

The test files are colocated with the source code in each package's src/ directory, with the filename format {name}.test.ts.

Build for production

To build Edge-CSRF for production, run the build command:

pnpm build

The build artifacts will be located in the dist/ directory of each package.

edge-csrf's People

Contributors

amorey avatar

Stargazers

Dominik Antal avatar Eugene Klimov avatar Ryota Sakai avatar dai avatar Clayton Kehoe avatar Kyle Esterhuizen avatar Florent Perez avatar J.Delauney avatar AK avatar Sebastian Sobociński avatar Antoni Kępiński avatar Patrik Pompe avatar Jackson Ricardo Schroeder avatar orange fritters avatar Martin Schwier avatar Jonathan Pritchard avatar Timm Stokke avatar Zain ✋ avatar branislavballon avatar Nicholas Sylke avatar Ryo Tozawa avatar Andy Qin avatar Daniel avatar Jack Dane avatar Andrey avatar Koki Ukai avatar Riccardo Venturini avatar Ryuk avatar  avatar Thomas Lekanger avatar Shuhei Shogen avatar Niclas Leon Bock avatar JOZEF ANTONY NEELAMKAVIL avatar Stefan avatar Salvador Briones avatar  avatar  avatar Bart Stefanski avatar Wojtek Wrotek avatar Mikail B. avatar Siya avatar Nick K. avatar Jason avatar Mehdi Baaboura avatar KY avatar Boris Burgos avatar Ricardo Monteiro avatar sonagi avatar Alex Younger avatar hanetsuki avatar Kazunori Ninomiya avatar C. T. Lin avatar Evan Ye avatar Matt Wigham avatar Alejandro avatar Kazuya Yamashita avatar Janis Jansons avatar Paul avatar Edoardo Ranghieri avatar omar avatar Yony Calsin avatar mrisho avatar Rabbil Hasan avatar flect_k_koi avatar Riordan avatar Ferdi ÜNAL avatar  avatar  avatar  avatar Zach Brown avatar Samet avatar undefined avatar  avatar Jim Johnston avatar Sinan Can Gulan avatar Thomas Liu avatar Garfield Linden avatar Viktor Sokolov avatar Kazım Madan avatar Stef avatar juan bz avatar Hidemi Yukita avatar omoogun olawale avatar Kento TSUJI avatar Phil Wolstenholme avatar Javier Maestre avatar Sohel Islam Imran avatar Thorge Wandrei avatar Adrien KISSIE avatar Rúben Amorim avatar Muhammad Abu 'l-Gharaniq avatar Sung Jeon avatar Matheus Verissimo avatar Harry Tran avatar kimihito avatar Giancarlo Buomprisco avatar  avatar Olivier Philippon avatar Shaik Noorullah Shareef avatar Damian Senn avatar

Watchers

 avatar  avatar

edge-csrf's Issues

Doesnt work with server action NextJS 14

Says invalid csrf token

 <form action={logout}>
                <Input
                  type="hidden"
                  className="hidden"
                  name="csrf_secret"
                  value={csrfToken}
                />

                <Button
                  type="submit"
                  variant="destructive"
                  className="w-full justify-start cursor-pointer"
                >
                  Logout
                </Button>
  </form>
const csrfProtect = csrf({
  cookie: {
    secure: process.env.NODE_ENV === "production",
    name: "csrf_secret",
  },
});

[Bug] Testing code that uses `edge-csrf` ends up in error `SyntaxError: Cannot use import statement outside a module`

Version
0.2.1

Steps to reproduce
The following public repository contains a minimum replication environment: https://github.com/erickpatrick/nextjs-jest-edge-csrf-minimum-reproduction

  • Clone the repository
  • Run npm install --force
    • --force is required as edge-csrf requires Next.js v12 though repo uses Next.js v13
    • error also happens on Next.js v12.x
  • Run npm run test

Description of the repository
The repository is a Next.js app. It was setup using the Jest Testing documentation from Next.js docs website.

The project contains the <rootDir>/myFile.ts file which uses edge-csrf. A companion test file, <rootDir>/myFyle.test.ts, contains a minimum test file where we try to test <rootDir>/myFile.ts functionality.

Actual behavior
When running npm run test, Jest throws the following error:

● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:

    /Users/dealveri/Code/test/node_modules/edge-csrf/dist/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { Config } from './config';
                                                                                      ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      7 |

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1449:14)
      at Object.<anonymous> (myfile.ts:9:56)
      at Object.<anonymous> (myfile.test.ts:7:17)

Expected Behavior
After running npm run test, Jest's logs from successful or failed test runs should display. In other words, the files from edge-csrf repository would provide files already transformed to a low level enough (CommonJS, perhaps) which would allow npm run test to behave as expected.

Additional Context
Attention to the **SyntaxError: Cannot use import statement outside a module** message. I've tried to follow Jest's documentation on how to add files from node_modules to be transformed, but it did not work.

I have also tried to define a babel.config.js like in the documentation and configuring it in order to transform the files from edge-csrf. However, it also did not work.

Environment
System:

  • ProductName: macOS
  • ProductVersion: 13.0.1
  • BuildVersion: 22A400

Binaries:

  • Node: 16.17 - /usr/local/bin/node
  • npm: 8.15.0 - /usr/local/bin/npm

Is there any way to protect only a specific path?

I would like to protect only my /api/ path, this means that I want to protect the GET method also
If I don't ignore the GET method, the csrf token will not be created and inserted in the header of a normal page.

How to work around this?

Is `edge-csrf` deprecated?

This morning we notice this warning:
image

Is there a specific reason why this package is entered in deprecation? Can you route us to an alternative direction?

Thanks.

undefined cookie value

I just tried to use the latest version of the library and noticed that the cookie value is not properly set. This results in the missing value of the csrf token.

next: 15.4.1

Please find screenshoots below:
Chrome:
Screenshot 2022-12-27 at 7 14 10 PM

Console:
Screenshot 2022-12-27 at 7 16 25 PM

Also, I noticed a warning from next: Middleware is returning a response body (line: 13), which is not supported.
Learn more: https://nextjs.org/docs/messages/returning-response-body-in-middleware

I hope my feedback helps! Thank you for your work on this library!

Invalid csrf token in server action when used without a form

It is possible to use server actions without using forms. https://react.dev/reference/react/use-server#calling-a-server-action-outside-of-form

In this case it still makes a POST request but when I pass csrfToken or csrf_token, csrf check fails.

Looking at the request, it makes POST request with the following body:
[{"csrf_token":"AAidYJqD9YPPT5NRqWt0l+czE4APICWVe+SV7RoP"}]

But weirdly Content-Type header is set to text/plain;charset=UTF-8 for all server actions.

Tested with edge-csrf 1.0.7 and nextjs 14.0.4

Next.js v14 Bump

Hi there,

thanks for this package!

Opening this issue just to report Next.js v14 has been released and needs a bump :)

Thank you!

Question: Usage of token in getServerSideProps

Hi, thank you for your work on this. I am trying to use your package in a project, and I'm having issues getting the token in the getServersSideProps method.

I tried to follow your examples but I never seem to get a token using res.getHeader("x-csrf-token") like in all of your examples. But when I use it like this:

export const getServerSideProps: GetServerSideProps = async (context) => {

	const csrfToken = context.req.headers["x-csrf-token"] ?? "missing";
	if (csrfToken === "missing") {
		throw new Error("Invalid CSRF token");
	}

	return { props: { csrfToken } };
};

I get a valid header and the route is protected. Can you tell me what is happening?

excludePathPrefixes config option is missing in dist folder

Hi!

I just installed version 1.0.3 of edge-csrf and it seems the excludePathPrefixes config option is missing in the packaged dist folder.

image

As a workaround, one can manually skip the invocation of csrfProtect in middleware.ts:

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();

  if (!req.nextUrl.pathname.startsWith('/_next/')) {
    csrfProtect(req, res);
  }

Cookie value not correctly checked against csrf token value

As long as a cookie and a csrf token (in header or body) exist, the verifyToken() check seems to return 'true'.

You can edit the cookie or csrf token to be of any value and the mutation request will still be sent.

We use the library as in the nextjs13-approuter-js-submission-static example. In middleware, we implement the functions as stated in the example. When making a PUT request with fetch(), we attach the token to the request body. We also tested with attaching it to the header, which seems to give the same result.

Example of cookie modification:
image

We tried to debug. The cookie value recieved by middleware is "test". In the verify() function hash is equal to hashCheck, which it probably shouldn't be.

We may have an implementation error or a misunderstanding of the library.

Please let me know if you need more information.

Next 13.3.x breaks edge-csrf

When using next 13.3.1 the token is not accessible in getServerSideProps from the response header. in 13.2.4 everything works fine.
See #7 for some context.

Storing in react-context?

This package is super helpful, thank you.

Do you by any chance have any opinions / ideas on storing the csrf token in react context? I'd like to do this for 2 reasons:

  1. Instead of having to pass it around subcomponents, I can pull it out of react context.
  2. I'm using react-query to make my API requests and often they are simply JSON requests. Ideally, I would just use the header defined approach to avoid having to pass it in as a key and polluting my API request body. Having it in the context would aid with that.

Does this present any security concerns?

Cross-domain CSRF support?

First thanks for this project. I'd like to add a cross-domain CSRF support. My use case is: domain a.com sends requests to domain api.b.com. Both a.com and api.b.com are Next.js projects (two different projects in separate repo). Is it possible to use edge-csrf to implement it?

Thanks!

Does not work in XHR requests if token passed in body

Currently I am unable to get edge-csrf working with server actions or regular ajax forms in Next.js 13.5.3.

It works only if I pass x-csrf-token in header, but it doesn't work if passed as csrf_token in form input. And while regular forms with XHR don't modify the input names, server actions also prefix the input names, so csrf_token becomes 1_csrf_token

Not sure if it is relevant, but I also upload files so the request is a POST with multipart body.
And I have set cookie httpOnly to false.

Does it work with Next.js 14.1.0?

Can some confirm edge-csrf is working with the current Next.js 14.1.0?

This is my middleware.js:

const csrfProtect = csrf({
  cookie: {
    secure: false,
  },
});

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

  console.log(`MIDDLEWARE: ${request.method} ${request.url}`);

  const csrfError = await csrfProtect(request, response);

  console.log(csrfError);

  return response;
}

When posting a form via a server actions I get this error:

MIDDLEWARE: POST http://localhost:3000/users/410
 ⨯ Error [TypeError]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type number (410)
    at Function.from (node:buffer:324:9)
    at atob (file:///app/node_modules/next/dist/compiled/edge-runtime/index.js:1:657096)
    at atou (webpack-internal:///(middleware)/./node_modules/edge-csrf/dist/esm/util.js:34:13)
    at eval (webpack-internal:///(middleware)/./node_modules/edge-csrf/dist/esm/index.js:34:118)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.middleware [as handler] (webpack-internal:///(middleware)/./src/middleware.js:20:23)
    at async adapter (webpack-internal:///(middleware)/./node_modules/next/dist/esm/server/web/adapter.js:176:16)
    at async runWithTaggedErrors (file:///app/node_modules/next/dist/server/web/sandbox/sandbox.js:99:24)
    at async DevServer.runMiddleware (file:///app/node_modules/next/dist/server/next-server.js:1039:24)
    at async DevServer.runMiddleware (file:///app/node_modules/next/dist/server/dev/next-dev-server.js:261:28)
    at async NextNodeServer.handleCatchallMiddlewareRequest (file:///app/node_modules/next/dist/server/next-server.js:323:26)
    at async DevServer.handleRequestImpl (file:///app/node_modules/next/dist/server/base-server.js:813:28)
    at async (file:///app/node_modules/next/dist/server/dev/next-dev-server.js:331:20)
    at async Span.traceAsyncFn (file:///app/node_modules/next/dist/trace/trace.js:151:20)
    at async DevServer.handleRequest (file:///app/node_modules/next/dist/server/dev/next-dev-server.js:328:24)
    at async handleRoute (file:///app/node_modules/next/dist/server/lib/router-utils/resolve-routes.js:319:33)
    at async resolveRoutes (file:///app/node_modules/next/dist/server/lib/router-utils/resolve-routes.js:539:28)
    at async handleRequest (file:///app/node_modules/next/dist/server/lib/router-server.js:200:96)
    at async requestHandlerImpl (file:///app/node_modules/next/dist/server/lib/router-server.js:366:13)
    at async Server.requestListener (file:///app/node_modules/next/dist/server/lib/start-server.js:140:13) {
  middleware: true
}
 POST /users/410 404 in 57ms
 POST /users/410 404 in 2ms
Error: Failed to find Server Action "e18922f36d5036b5f7f6212614582d6e60b7cdd6". This request might be from an older or newer deployment. Original error: Invariant: Couldn't find action module ID from module map.
    at t3 (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:1693)
    at /app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:7079
    at AsyncLocalStorage.run (node:async_hooks:346:14)
    at t4 (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:6387)
    at rk (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:25940)
    at /app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:28648
    at AsyncLocalStorage.run (node:async_hooks:346:14)
    at Object.wrap (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:36:16172)
    at /app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:28536
    at AsyncLocalStorage.run (node:async_hooks:346:14)
    at Object.wrap (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:36:15420)
    at r_ (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:28463)
    at rW.render (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:32559)
    at doRender (/app/node_modules/next/dist/server/base-server.js:1394:44)
    at cacheEntry.responseCache.get.routeKind (/app/node_modules/next/dist/server/base-server.js:1555:34)
    at ResponseCache.get (/app/node_modules/next/dist/server/response-cache/index.js:49:26)
    at DevServer.renderToResponseWithComponentsImpl (/app/node_modules/next/dist/server/base-server.js:1463:53)
    at /app/node_modules/next/dist/server/base-server.js:992:121
    at NextTracerImpl.trace (/app/node_modules/next/dist/server/lib/trace/tracer.js:104:20)
    at DevServer.renderToResponseWithComponents (/app/node_modules/next/dist/server/base-server.js:992:41)
    at DevServer.renderErrorToResponseImpl (/app/node_modules/next/dist/server/base-server.js:2114:35)
    at async pipe.req.req (/app/node_modules/next/dist/server/base-server.js:1988:30)
    at async DevServer.pipeImpl (/app/node_modules/next/dist/server/base-server.js:911:25)
    at async /app/node_modules/next/dist/server/dev/next-dev-server.js:331:20
    at async Span.traceAsyncFn (/app/node_modules/next/dist/trace/trace.js:151:20)
    at async DevServer.handleRequest (/app/node_modules/next/dist/server/dev/next-dev-server.js:328:24)
    at async invokeRender (/app/node_modules/next/dist/server/lib/router-server.js:163:21)
    at async handleRequest (/app/node_modules/next/dist/server/lib/router-server.js:357:24)
    at async requestHandlerImpl (/app/node_modules/next/dist/server/lib/router-server.js:366:13)
    at async Server.requestListener (/app/node_modules/next/dist/server/lib/start-server.js:140:13)

See also Issue #21

Sentry & CSRF

I’m using Sentry to monitor my FE application and edge-csrf to manage the CSRF security configurations.

Sentry sets up a /monitoring endpoint to report metrics / monitoring events. We can obviously exclude this endpoint from our middleware / CSRF checks, but I’m curious if there’s a known way to wrap those Sentry API calls in similar edge-csrf logic to ensure all the app’s endpoints are protected properly.

[Proposal] Abstracting Next.js away

Hi!

I use this package with Next.js - and would love to use it with other frameworks - so they can run well in edge runtimes.

I'd love to know if there would be any interest in abstracting this package away from Next.js and perhaps exporting a framework-less version - while using a different library (perhaps in an npm workspace) for exporting framework-specific implementations (Next.js, Remix, Sveltekit, etc.).

Happy to contribute if so!

CSRF on App not working

headers() only provides read-only request data and not from response data.

CSRF is attached to response headers.

Are you able to successfully get any csrf data from any server components in the app directory
just by using headers()?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.