GithubHelp home page GithubHelp logo

j0lv3r4 / next-csrf Goto Github PK

View Code? Open in Web Editor NEW
141.0 4.0 23.0 976 KB

CSRF mitigation for Next.js

Home Page: https://npm.im/next-csrf

License: MIT License

JavaScript 44.08% TypeScript 46.43% CSS 9.49%
csrf csrf-protection nextjs next node security

next-csrf's Introduction

next-csrf

Discord

CSRF mitigation for Next.js.

Features

Mitigation patterns that next-csrf implements:

Installation

With yarn:

yarn add next-csrf

With npm:

npm i next-csrf --save

Usage

Create an initialization file to add options:

// file: lib/csrf.js

import { nextCsrf } from "next-csrf";

const { csrf, setup } = nextCsrf({
    // eslint-disable-next-line no-undef
    secret: process.env.CSRF_SECRET,
});

export { csrf, setup };

Protect an API endpoint:

// file: pages/api/protected.js

import { csrf } from '../lib/csrf';

const handler = (req, res) => {
    return res.status(200).json({ message: "This API route is protected."})
}

export default csrf(handler);

Test the protected API route by sending a POST request from your terminal. Since this request doesn't have the proper token setup, it wil fail.

curl -X POST http://localhost:3000/api/protected
>> {"message": "Invalid CSRF token"}

Use an SSG page to set up the token. Usually, you use CSRF mitigation to harden your requests from authenticated users, if this is the case then you should use the login page.

// file: pages/login.js

import { setup } from '../lib/csrf';

function Login() {
    const loginRequest = async (event) => {
        event.preventDefault();
        
        // The secret and token are sent with the request by default, so no extra
        // configuration is needed in the request.
        const response = await fetch('/api/protected', {
            method: 'post'
        });
        
        if (response.ok) {
            console.log('protected response ok');
        }
    }
    
    return (
        <form onSubmit={loginRequest}>
            <label>
                Username
                <input type="text" required />
            </label>
            
            <label>
                Password
                <input type="password" required />
            </label>
            
            <button>Submit</button>
        </form>
    )
}

// Here's the important part. `setup` saves the necesary secret and token.
export const getServerSideProps = setup(async ({req, res}) => {
    return { props: {}}
});

export default Login;

API

nextCsrf(options);

Returns two functions:

  • setup Setups two cookies, one for the secret and other one for the token. Only works on SSG pages.
  • csrf Protects API routes from requests without the token. Validates and verify signatures on the cookies.

options

  • tokenKey (string) The name of the cookie to store the CSRF token. Default is "XSRF-TOKEN".
  • csrfErrorMessage (string) Error message to return for unauthorized requests. Default is "Invalid CSRF token".
  • ignoredMethods: (string[]) Methods to ignore, i.e. let pass all requests with these methods. Default is ["GET", "HEAD", "OPTIONS"].
  • cookieOptions: Same options as https://www.npmjs.com/package/cookie

next-csrf's People

Contributors

dependabot[bot] avatar revolunet avatar thegoleffect avatar yuriharrison avatar zbnauj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

next-csrf's Issues

publish the types?

This project was written in typescript yet it did not come with typings in npm package, kinda weird and preventing me from using it in my typescript project.

Next.js v13

Has anyone attempted using this on Nextjs V13? Just trying to see how others are using this, as now I’m just manually creating a cookie for the CSRF.

Issues on live server

Hi all,
I implement next-csrf and everything is ok on Local but when I deploy to server not working. I checked on Live server, Next-csrf not generate XSRF and csrfSecret cookies which can generate perfect on local.

image

I use Ubuntu 18 on VPS vultr.
Anyone got same issues

"Signed cookie string must be provided." with multiple cookies

Avoid calling 'unsign' in case if the token from cookie is 'undefined'. If the first argument is different from 'string', the unsign function will always throw "Signed cookie string must be provided."

src/cookies/getCookie.ts

function getCookie(req: NextApiRequest, name: string): string {
  if (req.headers.cookie != null) {
    const parsedCookie = parse(req.headers.cookie);
    return parsedCookie[name];
  }

  return "";
}

PR fix here

This function checks if headers.cookie !== null and is trying to get the token from parsed cookie, but the cookie could be present but different from XSRF token.

Let's say I'm also using google analytics (gtag) that is making its own cookies such us: _ga=GA1.1.1798070841.1638877244;

parsedCookie will be:

parsedCookie = {
  "_ga"="GA1.1.1798070841.1638877244"
}

and the return statement parsedCookie[name] where name is tokenKey (by default XSRF-TOKEN) will be undefined

then...

src/middleware/csrf.ts

const tokenFromCookie = getCookie(req, tokenKey);
const tokenFromCookieUnsigned = unsign(tokenFromCookie, secret); 

since tokenFromCookie is undefined, and the first argument must be typeof string, this function will always throw "Signed cookie string must be provided." and the request will fail with status 500

Env variable cant load on browser

As mentioned in #23, environment vars can't load on the browser (obviously). Ok then, how do I SECURELY load the secret for the browser, without exposing it? Is there any way?

I think, this exposes "secret" key to client's browser. Doesn't it?

I was looking for implementing csrf for nextjs and I came here from the npm module.

Doc usage says:

import App from 'next/app'
import { csrfToken } from '../lib/csrf';

function MyApp({ Component, pageProps }) {
return <Component {...pageProps, csrfToken} />
}

In my opinion, we shouldn't generate the CSRF token client side. Here, as you see in above, the CSRF token is generated client side. I think, this may expose the used "secret" key to the client's browser (as it is generated client side) and so once attacker get the access of "secret" key then multiple CSRF tokens can be generated.

Please correct me if I am wrong.

Internal server error

I legit copied the docs. I get internal server error. if i remove the csrf(func) wrapper the error goes away

"TypeError: Secret string must be provided."

I get this error when trying to use next-csrf while using the documentation 1:1. It thinks the CSRF_SECRET is empty, even though it is set in .env.

It works when I prepend "NEXT_PUBLIC_" to the env variable but this would expose the secret to the browser: https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser (and I can also see this in the source code)

This is my csrf.js:

// file: lib/csrf.js
import { nextCsrf } from "next-csrf";

const options = {
    secret: process.env.CSRF_SECRET
}

export const { csrf, csrfToken } = nextCsrf(options);

Pass context back to GetServerProps handler

Hello, i'm trying to get the query from the context argument of getServerSideProps. But i can't achieve this if i follow the example you provided.

export const getServerSideProps = setup(handler)

For setup function is passing req and res to handler and not the full context object which includes query property as well.

 const req = isApi
    ? (args[0] as NextApiRequest) // (*req*, res)
    : (args[0] as GetServerSidePropsContext).req; // (context).req
  const res = isApi
    ? (args[1] as NextApiResponse) // (req, *res*)
    : (args[0] as GetServerSidePropsContext).res; // (context).res
    
    // ...
    
    return handler(req as NextApiRequest, res as NextApiResponse);

that function seems to be implemented to work on pages/ on getServerSideProps so is there any point from typing the handler to be NextApiHandler instead of GetServerSideProps? and how can i get the query property?
Thanks.

Broken middleware

I'm having some problems getting the package to work and debugging it I found some issues on the middleware.

  1. First one, middleware is breaking when trying to parse a cookie. There is no reason to parse anything here because getToken already returns the expected value for tokenFromHeaders.
    https://github.com/j0lv3r4/next-csrf/blob/6f6de6b8269d1af785509680b73f53305f6ffe03/src/middleware/csrf.ts#L46

PR to fix it

  1. Another issue is the way you're using the csrf/Token package, the way it is setup right now every time a rebuild happens the csrfSecret changes and all open sessions start to fail on the tokens.verify check...

https://github.com/j0lv3r4/next-csrf/blob/6f6de6b8269d1af785509680b73f53305f6ffe03/src/middleware/csrf.ts#L69

and I need to reset the cookies (manually) to get it working again. That is annoying during development but not just that, the way it is right now you can't scale the application because each instance will be handling a different csrfSecret.

Possible solutions:
Use the same process.env.CSRF_TOKEN parameter when generating tokens on csrf/Token package;
Or accept a csrfSecret as a required parameter.

  1. And lastly, I think passing the first request forward as if it was a valid request is an security issue. How would I protect the first request? I think it should be required a call to a different endpoint (not csrf protected) then it generates the first cookie and all csrf protected endpoints should be restricted on all calls.

Typo in file name

Thanks for writing this.

When trying to use this, I'm finding that the documentation may be lacking some information. But following it I found this issue:

TypeError: Cannot read property 'inherits' of undefined
.../node_modules/next-csrf/dist/next-csrf.js:107:21)

I installed the repo locally and found that /src/csrf/indext.ts should be /src/csrf/index.ts without the t after the x.

TypeError: Cannot read property 'inherits' of undefined

Hello I tried your package and received the error from the subject bar today, and ideas for a workaround?

TypeError: Cannot read property 'inherits' of undefined
    at Object.<anonymous> (/home/yourname/Elas/base/node_modules/next-csrf/dist/next-csrf.js:107:21)

Thanks

Missing HttpOnly Cookies with setup() Wrapper Usage

I've encountered an issue where HttpOnly cookies are not present when using the setup() wrapper. This is problematic as I need access to server-side cookies for my application to function correctly. Could someone guide me on how to ensure HttpOnly cookies are accessible when utilizing the setup() method? Any advice or workaround would be greatly appreciated.

Thank you in advance.

Do I use correctly next-csrf ??

sorry for posting a question here, but I don't know if it's me who makes a mistake or is it problem with a package.
My application doesn't have a login or anything like this, so I created an empty endpoint that only sets a cookie when entering the page, and then I submit a form

My setup:

// service/csrf.js
import { nextCsrf } from "next-csrf";

const CSRF_SECRET = process.env.CSRF_SECRET;
const options = {
  secret: CSRF_SECRET
};

export const { csrf, csrfToken } = nextCsrf(options);
//pages/api/csrf.js
import { csrf } from "service/csrf";

async function handler(req, res) {
  return res.status(200).end();
}

export default csrf(handler);
//pages/_app.js
import { csrfToken } from "service/csrf";

function MyApp({ Component, pageProps }) {
  return (
      <Component {...pageProps} csrfToken={csrfToken} />
  );
}
export default MyApp;
// pages/index.js
export default function HomePage({
  csrfToken,
}) {
  useEffect(() => {
  // first request to set a cookie
    apiRequest({
      method: httpMethods.GET,
      url: "api/csrf",
      headers: {
        "XSRF-TOKEN": csrfToken,
      },
    });
  }, []);

  return (
          <Form
            csrfToken={csrfToken}
          />
  );
}
const Form = ({ csrfToken }) => {
  const handleSubmit = (event) => {
    event.preventDefault();
    apiRequest({
      method:  "POST",
      url: `api/send-form`,
      data: {
          //....
      },
      headers: {
        "XSRF-TOKEN": csrfToken,
      },
    })
      .then(() => {
      })
      .catch((error) => {
      });
  };

  return (
        <form onSubmit={handleSubmit}>
             //....
        </form>
  );
};
// pages/api/send-form.js

import { csrf } from "service/csrf";

async function handler(req, res) {
 // ....
}

export default csrf(handler);

It returns 403 at this moment https://github.com/j0lv3r4/next-csrf/blob/main/src/middleware/csrf.ts#L64

I changed a package in node module to reflect the last changes that where made in this PR #31 because there is no new version of package with those changes.

how to use in API endpoint

On the API page but my requests with XSRF-TOKEN header turned out to be error 500, can anyone help?
I tried console.log csrf normally comes out csrf but why can't request.

// pages/api/yt.js
const ytdl = require('ytdl-core');
import { csrf } from '@/lib/csrf';

async function handler(req, res) {
  if (req.method === 'POST') {
    try{
      const url  = req.body.url;
      const type = req.body.type;
      if(type === "mp3"){
        res.setHeader('content-type', "audio/mpeg");
        await ytdl(url, {
          format: 'mp3',
          filter: 'audioonly',
        }).pipe(res);
      }else if(type === "mp4"){
        res.setHeader('content-type', "video/mp4");
        await ytdl(url).pipe(res);
      }
    }catch(err){
      console.log('err: ', err);
    }
  } else {
    res.status(400).json({ result: false })
  }
}

export default csrf(handler);

if i don't use csrf then it's safe no error but when i use ..csrf(handler) it will be problematic

// components/YT.js
...
const requestOptions = {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'XSRF-TOKEN': csrfToken },
  body: JSON.stringify({ url, type:"mp4" })
};
console.log(requestOptions);
fetch(`${domain}/api/yt`, requestOptions)
.then( res => res.blob() )
.then( blob => {
  const sizeInBytes = blob.size;
  setShowProcess(false);
  if(sizeInBytes <=0){
    setInfo(["Unable to download! Maybe File size is too high. Try to download video less than 5MB", 2]);
  }else{
    download(blob,`${title}.mp4`,"video/mp4");
    setInfo(["Ready for download!", 3]);
  }
});
...

Env cannot load into _app.ts

Following the example given in your README, there is one issue regarding the csrfToken being passed down as props in the custom app. Whenever I try to pass it down, it will give me the error Module not found: Can't resolve 'fs' in [DIRECTORY] Error from chokidar (C:\): Error: EBUSY: resource busy or locked, lstat 'C:\hiberfil.sys' which indicates to me there is an issue loading the .env into the module. In addition, there is an error thrown here:
TypeError: Cannot read property 'inherits' of undefined at Object.<anonymous> (.....\node_modules\next-csrf\dist\next-csrf.js:107:21)

Next.js version: next@latest

How to protect api routes on first request?

Just checking out this library and trying to understand it.

In a classic CSRF scenario, a user navigates to evil.com which has an img tag that makes a request directly to one of my protected api routes, e.g., <img src="https://my/protected/api/route />, how does the middleware protect against this kind of CSRF? Looking at the code in the middleware, if there's no cookie, it simply attempts to set the cookie and forwards the handler, resulting in a successful request?

    // If no token in cookie then we assume first request and proceed to setup CSRF mitigation
    if (!tokenFromCookie) {
      const reqCsrfToken = tokens.create(csrfSecret);
      const reqCsrfTokenSigned = sign(reqCsrfToken, secret);

      res.setHeader(
        "Set-Cookie",
        serialize(tokenKey, reqCsrfTokenSigned, cookieOptions)
      );
      return handler(req, res);
    }

Maybe I'm misunderstanding or missing some setup? Thanks.

The implementation probably defeats the purpose of CSRF protection

In the server side, if you read the CSRF token value from cookie and do the validation, I don't think it protects you from CSRF attacks.

Let's say, on attacker's website, they have a form like this.

<!--- A form on https://attackers-site.com -->
<form action="https://yoursite.com/api/protected" method="POST">
    ....
</form>

When this form is submitted, the browser will send the actual cookies of yoursite.com (including the CSRF related cookies) with the request. The server will always consider it valid because it's sending the previously set CSRF (valid) cookie.

I recommend the following changes:

  • Not validating CSRF token from cookie, instead provide a way to pass the token to the client.
  • Client side can can decide how to send the token along with a POST request and server should handle it accordingly. For example, <form> can send it as hidden input. Ajax requests can send it in the header.
  • Making the corresponding cookie HttpOnly should be optional. This way client can access it using JS and add it to the headers of Ajax requests.

Header ‘xsrf-token’ is not allowed

Hello!
I used next-csrf like your example and I am getting the follow error in my console:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/articles. (Reason: header ‘xsrf-token’ is not allowed according to header ‘Access-Control-Allow-Headers’ from CORS preflight response).

I also set headers in my next.config.js file like:

const securityHeaders = [
    {
        key: "Access-Control-Allow-Headers",
        value: "X-CSRF-Token"
    }
]

module.exports = {
    async headers() {
        return [
            {
                source: '/(.*)',
                headers: securityHeaders,
            },
        ]
    },
}

Failed to read secret cookie in setup function

I hit a weird edge case were the getServerSideProps function get executed twice:
vercel/next.js#13064

The problem is there seems to be a bug in the getSecret function. For some reason it does a toLowerCase on the tokenKey which makes it so it fails to read the existing csrfSecret cookie and it just create a new one, this created a race condition for me when I had a POST call happen to my Next server in between the two setup calls and the XRSF-TOKEN created by the POST call was created with wrong secret. Just removing the toLowerCase will fix the issue.

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.