Как реализовать защиту CSRF в Nextjs с Apollo и GraphQL

Следуя этому примеру в репозитории Nextjs, я хочу для реализации защиты CSRF (возможно, с помощью пакета csurf), потому что я использую файл cookie идентификатора сеанса с экспресс- -сессия.

Я попытался установить csurf на своем настраиваемом сервере и сохранить сгенерированный токен в res.locals.csrfToken, который можно использовать при загрузке первой страницы статическим методом getInitialProps, который находится в /lib/withApollo.js в примере I связаны. Как только я пытаюсь изменить страницу (со ссылками) или попытаться сделать почтовый запрос с помощью apollo (например, войти в систему), сервер меняет токен csrf, поэтому тот, который использовался Apollo, больше не полезен, и поэтому я получаю ошибка "csrf is invalid".

Пользовательский сервер с конфигурацией csurf

const csrf = require('csurf');
const csrfProtection = csrf();
////express-session configuration code////
app.use(csrfProtection);
app.use((req, res, next) => {
    res.locals.csrfToken = req.csrfToken();
next();
})

/lib/initApollo.js

function create(initialState, { getToken, cookies, csrfToken }) {
  const httpLink = createHttpLink({
    uri: "http://localhost:3000/graphql",
    credentials: "include"
  });

    const authLink = setContext((_, { headers }) => {
    const token = getToken();
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : "",
        Cookie: cookies ? cookies : "",
        "x-xsrf-token": csrfToken ? csrfToken : ""
      }
    };
  });

/lib/withApollo.js

static async getInitialProps(ctx) {
  const {
    Component,
    router,
    ctx: { req, res }
  } = ctx;
  const apollo = initApollo(
    {},
    {
      getToken: () => parseCookies(req).token,
      cookies: req ? req.headers.cookie : "",
      csrfToken: res ? res.locals.csrfToken : document.cookie
    }
  );

С этой конфигурацией каждый маршрут защищен от csrf, но токен, созданный на сервере, часто меняется, и Apollo не может получить обновленный, как только ему нужно, поэтому первая загрузка успешна, но последующее изменение страницы (ссылки) или любой почтовый запрос не выполняется из-за изменения токена.


person The_Wolf    schedule 13.01.2019    source источник
comment
Вы получили решение? У меня такая же проблема   -  person Shifut Hossain    schedule 12.06.2019
comment
Я тоже здесь застрял. Любые идеи??   -  person Ngatia Frankline    schedule 11.07.2019


Ответы (3)


Обновлять

После стольких просмотров я наконец смог отправить csrf cookie. Я думаю, что проблема связана со словом return. Когда вы используете return, он исключает cookie. Это то, что я сделал, отредактировав /lib/initApollo.js.


function create(initialState, { getToken, cookies, csrfToken }) {
  const httpLink = createHttpLink({
    uri: "http://localhost:3000/graphql",
    credentials: "include"
  });

    const authLink = setContext((_, { headers }) => {
    const token = getToken();
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : "",
        "x-xsrf-token": csrfToken ? csrfToken : ""
      }
      cookies: {
        ...cookies
      }
    };
  });

пре !! Однако SSR не имеет файлов cookie. Я думаю, у нас должно быть две конечные точки от клиента и еще одна для SSR. URL-адрес SSR может быть исключен из csrf.

person Ngatia Frankline    schedule 27.07.2019

Возможно, это не тот ответ, который вы ищете. Я прочитал здесь, что если вы используете JWT там Нет необходимости в CSRFToken. Не совсем уверен, но пока это единственное начало.

Бенджамин М объясняет следующее:

Я нашел информацию о CSRF + без использования файлов cookie для аутентификации:

https://auth0.com/blog/2014/01/07/angularjs-authentication-with-cookies-vs-token/ "поскольку вы не полагаетесь на файлы cookie, вам не нужно защищаться от межсайтовых запросов"

http://angular-tips.com/blog/2014/05/json-web-tokens-introduction/ «Если мы пойдем по пути файлов cookie, вам действительно нужно будет выполнять CSRF, чтобы избежать межсайтовых запросов. Это то, о чем мы можем забыть при использовании JWT, как вы увидите. " (JWT = Json Web Token, аутентификация на основе токенов для приложений без сохранения состояния)

http://www.jamesward.com/2013/05/13/securing-single-page-apps-and-rest-services «Самый простой способ выполнить аутентификацию, не рискуя уязвимостью CSRF, - просто избегать использования файлов cookie для идентификации пользователя»

http://sitr.us/2011/08/26/cookies-are-bad-for-you.html "Самая большая проблема с CSRF заключается в том, что файлы cookie не обеспечивают абсолютно никакой защиты от этого типа атак. Если вы используете аутентификацию файлов cookie, вы также должны принять дополнительные меры для защиты против CSRF. Самая простая мера предосторожности, которую вы можете предпринять, - убедиться, что ваше приложение никогда не выполняет никаких побочных эффектов в ответ на запросы GET ".

Есть еще много страниц, на которых указано, что вам не нужна защита CSRF, если вы не используете файлы cookie для аутентификации. Конечно, вы все еще можете использовать файлы cookie для всего остального, но не храните в них что-либо вроде session_id.

Полная статья здесь: Необходим токен CSRF при использовании аутентификации без сохранения состояния (= без сеанса)?

person Ngatia Frankline    schedule 11.07.2019

Для тех, кто не использует экспресс-сессию, у меня также работает приведенный ниже код. Надеюсь, это поможет другим, кому это может понадобиться. Я использую собственный сервер Express, и вот упрощенная версия моей реализации.

Server.js (пользовательский экспресс-сервер)

const express = require('express');
const next = require('next');
const url = require('url');
var csrf = require('csurf');
const cookieParser = require('cookie-parser');

// NextJS Configuration
const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev });
const handle = nextApp.getRequestHandler();

// Initiate the Express app
const PORT = process.env.PORT || 5000;
const app = express();

// CSRF protection middleware
var csrfProtection = csrf({ cookie: true });

// Initiate the NextApp
nextApp.prepare().then(() => {
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(cookieParser(process.env.COOKIE_PARSER_SECRET));

  // If you do not want your API routes protected with CSRF tokens, do not include the middlware
  app.use('/api/v1/wide-open', (req, res, next) => {
    return res.status(200).json({ message: 'This route is wide open' });
  });

  // If you want your API routes protected with CSRF
  app.use('/api/v1/protect-me', csrfProtection, (req, res, next) => {
    res.status(200).json({
      message: 'I am very safe',
    });
  });

  // Initialize CSRF to send a token to the front-end
  app.use(csrf({ cookie: true }));

  //catch-all for nextJS /pages
  app.get('*', (req, res) => {
    res.set({
      'Cache-Control': 'public, max-age=3600',
    });

    // It is important that the below two lines are inserted within the app.get('*') route
    const token = req.csrfToken();
    res.cookie('XSRF-TOKEN', token);

    const parsedUrl = url.parse(req.url, true);
    return handle(req, res, parsedUrl);
  });

  app.listen(PORT, (err) => {
    if (err) throw err;
    console.log('listening on port ' + PORT);
  });
});

Затем мы можем получить клиентскую часть XSRF-TOKEN из document.cookie в _app.js

_app.js

import React, { useEffect } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import Head from 'next/head';

export default function MyApp(props) {
  const { Component, pageProps } = props;

  useEffect(() => {
    // Get the XSRF-TOKEN from cookies
    function getCookie(name) {
      const value = `; ${document.cookie}`;
      const parts = value.split(`; ${name}=`);
      if (parts.length === 2) return parts.pop().split(';').shift();
    }

    // set the 'csrf-token' as header on Axios POST requests only (please see csurf docs to see which other headers they accept)
    // you could also add PUT or PATCH if you wish
    axios.defaults.headers.post['csrf-token'] = getCookie('XSRF-TOKEN');

    // The rest of your UseEffect code (if any).....
  }, []);

  // Your app
  return (
    <React.Fragment>
      <Head></Head>
      <Navbar />
      <Component {...pageProps} />
      <Footer />
    </React.Fragment>
  );
}

MyApp.propTypes = {
  Component: PropTypes.elementType.isRequired,
  pageProps: PropTypes.object.isRequired,
};

Единственное, в чем я не уверен, это то, имеет ли передача токена во внешний интерфейс какие-либо последствия для безопасности? У меня всегда было впечатление, что CSRF обрабатывается только на стороне сервера. Однако в документации для csurf есть примеры для React, где они передают его либо в тело запроса, либо в заголовок. Может быть, кто-нибудь, кто разбирается в вопросах безопасности, поделится своим опытом?

Поскольку мы не используем сеансы, сервер генерирует два токена, один из которых называется _csrf - это нормально, поскольку это секрет, по которому csurf будет проверять.

Примечания. Если вы реализуете это таким образом и тестируете в Postman / Insomnia, обычный запрос POST будет отклонен промежуточным программным обеспечением csurf. Итак, сначала вам нужно выполнить запрос GET на свой веб-сайт (или http: // localhost: PORT в dev) и получить токен csrf из возвращаемого файла cookie. Это немного раздражает, поэтому вы можете удалить промежуточное ПО, пока находитесь в режиме разработки, и обязательно добавить его, прежде чем переходить к производству.

person BleddP    schedule 06.04.2021