【问题标题】:How to implement CSRF protection in Nextjs with Apollo and GraphQL如何使用 Apollo 和 GraphQL 在 Nextjs 中实现 CSRF 保护
【发布时间】:2019-06-07 15:46:00
【问题描述】:

在 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保护了,但是服务器上创建的token经常发生变化,Apollo无法在需要的时候立即检索更新的token,所以第一次加载成功,但是后续页面发生变化(链接)或任何发布请求失败,因为令牌已更改。

【问题讨论】:

  • 您找到解决方案了吗?我也面临同样的问题
  • 也卡在这里。有什么想法吗??

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


【解决方案1】:

更新

经过这么多浏览后,我终于能够发送 csrf cookie。我认为问题出在return这个词上。当你使用return时,它会排除cookie。这就是我编辑/lib/initApollo.js所做的。

函数创建(初始状态,{ getToken,cookies,csrfToken }){ 常量 httpLink = createHttpLink({ uri: "http://localhost:3000/graphql", 凭据:“包括” }); const authLink = setContext((_, { headers }) => { 常量令牌 = getToken(); 返回 { 标题:{ ...标题, 授权:令牌? `承载 ${token}` : "", “x-xsrf-token”:csrfToken ? csrfToken:“” } 饼干: { ...饼干 } }; }); });

喂!!但是 SSR 没有 cookie。我认为我们应该有两个来自客户端的端点和另一个用于 SSR。 SSR url可以免csrf。

【讨论】:

    【解决方案2】:

    这可能不是您正在寻找的答案。我已经阅读了here,如果您使用的是 JWT,则不需要 CSRFToken。我不完全确定,但它是目前唯一可行的方法。

    Benjamin M解释如下:

    我发现了一些关于 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,一种基于 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 请求。”

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

    全文在这里:CSRF Token necessary when using Stateless(= Sessionless) Authentication?

    【讨论】:

      【解决方案3】:

      对于那些不使用 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 body 或 header。也许有更多安全见解的人可以分享他们的专业知识?

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

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

      【讨论】:

        猜你喜欢
        • 2019-02-03
        • 2019-12-31
        • 2022-06-18
        • 2014-09-23
        • 2020-08-10
        • 2017-11-22
        • 2020-03-21
        • 2020-08-27
        • 2023-03-31
        相关资源
        最近更新 更多