如何使用Apollo和GraphQL在Nextjs中实现CSRF保护

时间:2019-01-13 11:04:49

标签: graphql csrf apollo next.js express-session

在Nextjs存储库中,在this example之后,我想实现CSRF保护(也许使用csurf包),因为我正在将带有Express-Session的会话ID cookie用于

我尝试在自定义服务器中设置csurf并将生成的令牌保存在res.locals.csrfToken中,该文件可以在第一页加载时通过位于/lib/withApollo.js中的静态方法“ getInitialProps”获取我链接的示例。当我尝试更改页面(带有链接)或尝试使用apollo(例如登录)发出发布请求时,服务器更改了csrf令牌,因此Apollo使用的令牌不再有用,因此我得到了“ csrf无效”错误。

具有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无法在需要时立即检索更新的令牌,因此第一次加载成功,但是随后的页面更改(链接)或任何发布请求失败,因为令牌已更改。

3 个答案:

答案 0 :(得分:0)

这可能不是您要找的答案。我已经读过here,如果您使用的是JWT,则不需要CSRFToken。尚不确定,但这是目前唯一的解决方法。

Benjamin M解释如下:

我没有使用cookie进行身份验证就发现了有关CSRF +的一些信息:

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令牌,这是针对无状态应用程序的基于令牌的身份验证)

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请求做出任何副作用。”

还有更多页面,如果您不使用Cookie进行身份验证,则表明您不需要任何CSRF保护。当然,您仍然可以将Cookie用于其他所有内容,但要避免在其中存储诸如session_id之类的内容。

此处全文:CSRF Token necessary when using Stateless(= Sessionless) Authentication?

答案 1 :(得分:0)

更新

经过如此多的浏览,我终于能够发送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。 SSR网址可以被csrf豁免。

答案 2 :(得分:0)

对于那些不使用 express-session 的人,下面的代码也适用于我。我希望这可以帮助可能需要它的其他人。我使用的是自定义 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);
  });
});

然后我们可以从 _app.js 中的 document.cookie 获取 XSRF-TOKEN 客户端

_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 的例子,他们将它传递给 req 主体或标题。也许有更多安全见解的人可以分享他们的专业知识?

因为我们没有使用会话,服务器会生成两个令牌,一个称为 _csrf - 这是正常的,因为这是 csurf 将验证的秘密。

<块引用>

注意事项 如果您以这种方式实现它并且您正在 Postman / Insomnia 中进行测试,则 csurf 中间件将拒绝常规 POST 请求。因此,您首先必须向您的网站(或开发中的 http://localhost:PORT)发出 GET 请求,并从它返回的 cookie 中获取 csrf 令牌。这有点烦人,因此您可以在开发模式下删除中间件,并确保在进入生产模式之前将其添加回来。