CSRF令牌生成

50

这是一个关于生成CSRF令牌的问题。

通常我希望基于与用户会话相关的唯一数据生成令牌,并使用密钥进行哈希和盐处理。

我的问题是关于在没有唯一用户数据可用的情况下生成令牌。没有可用的会话,无法使用cookie,IP地址和其他类似信息也不可靠。

是否有任何理由不能将待哈希字符串包括在请求中呢?以下是生成令牌并嵌入其中的示例伪代码:

var $stringToHash = random()
var $csrfToken = hash($stringToHash + $mySecretKey)
<a href="http://foo.com?csrfToken={$csrfToken}&key={$stringToHash}">click me</a>

示例服务器端 CSRF 令牌验证

var $stringToHash = request.get('key')
var $isValidToken = hash($stringToHash + $mySecrtKey) == request.get('csrfToken')

在哈希中使用的字符串每次请求都会不同。只要它包含在每个请求中,CSRF令牌验证就可以继续进行。由于它每次请求都是新的,并且只嵌入在页面中,因此无法从外部访问令牌。然后,令牌的安全性取决于$mySecretKey仅为我所知。

这是一种天真的方法吗?我是否遗漏了某些原因,导致它无法正常工作?

谢谢


12
所提出的解决方案容易受到重放攻击。相同的令牌和密钥组合将无限期地起作用。 - Matthew
很好的观点,@Matthew。但是我们如何防止这种情况发生,即令牌是由服务器生成的,但用户没有回访我们的服务器,而是由具有相同sessionId+hash的黑客完成的?或者这是不可能的(除非比较ip地址/用户代理等)? - Nigrimmist
1
哦,hash(payload + key)容易受到长度扩展攻击。如果必须使用,请使用HMAC来实现。@Nigrimmist,请指定一个过期时间并将其纳入令牌中。 - Matthew
9个回答

29

是否有任何理由我不能把字符串作为请求的一部分包含在内进行哈希呢?

CSRF令牌有两个部分。一个是嵌入表单中的令牌,另一个是对应的令牌,可以是存在于cookie、会话或其他位置。这种“其他位置”的使用防止了页面自成体系。

如果在请求中包含要哈希的字符串,则该请求就是自成体系的,因此攻击者只需要复制表单即可,因为他们拥有令牌的两个部分,从而没有保护。

即使将其放在表单URL中,也意味着它是自成体系的,攻击者只需复制表单和提交URL。


17
不,你不需要。可以通过 cookie 保留其中一半内容,或直接将其丢弃。这些内容不需要被存储在服务器上,通常基于 cookie 实现,因此无需依赖启用会话功能。 - blowdart
1
根据OWASP,这只是一种名为“双重提交cookie”的缓解措施 https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet#Double_Submit_Cookies - zb226
你能解释一下这句话吗?“如果您在请求中包含要哈希的字符串,则该请求是自包含的,因此复制表单就是攻击者需要做的全部工作,因为他们拥有令牌的两个部分,因此没有保护措施。” - Suraj Jain

9

2

我认为基于HMAC的哈希是最好的想法,即使用一些密码加密哈希序列:用户名+用户ID+时间戳。每次请求哈希必须不同,时间戳也必须不同,否则可能会在攻击中简单地重放哈希。


1
我想说你的方法是有效的,因为CSRF攻击是攻击者利用受害者的浏览器来伪造已登录状态,他们为什么能够这样做呢?因为在大多数服务器端,会话检查是基于cookie中的SessionID进行的,而cookie是一段数据,会自动附加到发送给服务器的HTTP请求中。
因此,防御CSRF有两个关键因素:
1. 生成一个挑战令牌,并要求客户端以非cookie方式将其传递给服务器,可以是URL参数或POST表单都可以。 2. 像对待SessionID一样保护令牌的安全性,例如使用SSL。
我建议阅读CSRF Prevention Cheat Sheet

1
CSRF令牌旨在防止(无意的)数据修改,这通常是通过POST请求应用的。
因此,每个更改数据的请求(无论是GET请求还是POST请求),都必须包含CSRF令牌。

我的问题与生成令牌有关,当没有唯一的用户数据可用时如何生成。没有会话可用,cookie不是一个选择,IP地址和类似的东西也不可靠。

然后为每个访问者创建一个唯一的用户ID。将该ID包含在cookie或URL中(如果禁用了cookie)。

编辑:

考虑以下事件:

您已登录到Facebook帐户,然后进入某个任意网站。

该网站有一个表单,您提交该表单,告诉浏览器向您的Facebook帐户发送POST请求。

由于Facebook应用程序将您识别为注册并已登录的用户,因此该POST请求可能会更改您的密码或添加评论等(除非有其他阻止机制,例如CAPTCHA)。


将令牌的一部分添加到URL中,另一半放在表单中意味着根本没有保护。 - blowdart
当然,你说把id放在URL里或者cookie里(如果禁用了cookie)。但是把id放在URL里并不安全。 - blowdart
我找不到更好的方法来存储会话 ID,当 cookie 被禁用时。 - Dor
啊,你的意思是把会话 ID 放在 URL 中,然后在会话中保留 CSRF 令牌的一半?当你谈到唯一用户 ID 时,我以为你指的是用于生成令牌的唯一值。非常抱歉。 - blowdart

1

你只需要在URL/表单和cookie中使用相同的“token”。这意味着你可以通过JavaScript将页面设置为“token” cookie(最好是一些随机值),然后在发送到服务器的所有请求中传递完全相同的值(作为URI ?param或表单字段)。无需让服务器生成cookie。

只要我们相信浏览器不允许来自一个域的页面编辑/读取其他域的cookie,这就是安全的,而且今天被认为是相当安全的。

让服务器生成令牌将假定该令牌可以安全地传输到浏览器,而不会被任何CSRF尝试拾取(为什么要冒险?)。虽然你可以将更多的逻辑放入服务器生成的令牌中,但为了防止CSRF,没有必要。

(如果我在这里错了,请告诉我)


1
有多种CSRF令牌的实现方式。关键是确定此CSRF令牌是在客户端还是服务器端生成。因为这两种情况下实现方式和令牌熵值都会发生巨大变化。
对于服务器端,SecureRandom是首选方式。但在您的情况下,您希望在任何用户被识别之前生成CSRF令牌,window.crypto提供了此功能,您可以生成一个足够难以猜测的字符串用于CSRF令牌。

1

通过CSRF令牌的帮助,我们可以确保传入的请求已经过身份验证(知道用户不是黑客)

请注意,我需要以下方法,但谷歌甚至在stackoverflow上都无法帮助我找到下面提到的代码,但在收集了stackoverflow答案之后,我终于搞定了。所以这对进一步搜索/特别是初学者很有用

我已经描述了Spring MVC与Spring拦截器

注意-我使用Google缓存将salt存储在缓存中进行重新验证

需要在pom.xml中添加以下依赖项

    <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>28.0-jre</version>
    </dependency>

以下是HandlerInterceptorAdapter的实现。

    package com.august.security;

    import java.security.SecureRandom;
    import java.util.Enumeration;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.concurrent.TimeUnit;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;

    import org.apache.commons.lang3.RandomStringUtils;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

    import com.google.common.cache.Cache;
    import com.google.common.cache.CacheBuilder;

    public class CsrfSecurity extends HandlerInterceptorAdapter {
        List<String> urlList= new LinkedList<>();
        private static final String CSRF_TAG = "CSRF-CHECK";

        @SuppressWarnings("unchecked")
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handleer)
                throws Exception {
            System.out.println("Inside Pre Handler");

            String reqUrl = request.getRequestURI().toString();
            System.out.println("Request URL : " + reqUrl);
            String ipAddress = request.getHeader("X-FORWARDED-FOR");
            if (ipAddress == null) {
                ipAddress = request.getRemoteAddr();
            }
            //local host url http://localhost:8080/august/
            if (request.getRequestURI().contains("/august/")) {
                System.out.println("pre handler return true");
                //it will return and next executed postHandelr method
                //because of on above url my webApplication page working
                return true;
            }
            if (ignoreUrl().contains(request.getRequestURI())) {
                System.out.println("inside ignore uri");
                return true;
            } else {
                System.out.println("CSRF Security intercepter preHandle method started.......");
                String salt = request.getParameter("csrfPreventionSalt");
                HttpSession sessionAttribute = request.getSession();
                Cache<String, Boolean> csrfPreventionSalt = (Cache<String, Boolean>) sessionAttribute
                        .getAttribute("csrfPreventionSalt");
                if (csrfPreventionSalt == null) {
                    System.out.println("Salt not matched session expired..");
                    parameterValuesPrint(request, "saltCacheNotFound");
                    response.sendRedirect("error");
                    return false;
                } else if (salt == null) {
                    parameterValuesPrint(request, "noSaltValue");
                    System.out.println("Potential CSRF detected !! inform ASAP");
                    response.sendRedirect("error");
                    return false;
                } else if (csrfPreventionSalt.getIfPresent(salt) == null) {
                    System.out.println("saltValueMisMatch");
                    System.out.println("Potential CSRF detected !! inform ASAP");
                    response.sendRedirect("error");
                } else {
                    request.setAttribute("csrfPreventionSalt", csrfPreventionSalt);
                }
                return true;
            }

        }

        @SuppressWarnings("unchecked")
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                ModelAndView modelAndView) {
            System.out.println("Inside post Handler");
            System.out.println("CSRF Security key generator method started");
            try {
                //localhost url http://localhost:8080/august/
                //api is my controller path so no need to genrate token for api
                if (request.getRequestURI().contains("/august/api/")) {
                    System.out.println("No need to genrate salt for api");
                } else {
                    HttpSession sessionAttribute = request.getSession();
                    Cache<String, Boolean> csrfPreventionSaltCache = (Cache<String, Boolean>) sessionAttribute
                            .getAttribute("csrfPreventionSalt");
                    System.out.println("csrfPreventionSaltCache ::: " + csrfPreventionSaltCache);
                    if (csrfPreventionSaltCache == null) {
                        csrfPreventionSaltCache = CacheBuilder.newBuilder().maximumSize(5000)
                                .expireAfterWrite(20, TimeUnit.MINUTES).build();
                        request.getSession().setAttribute("csrfPreventionSaltCache", csrfPreventionSaltCache);
                    }

                    String salt = RandomStringUtils.random(20, 0, 0, true, true, null, new SecureRandom());
                    System.out.println("csrfPreventionSalt genrated ::: " + salt);
                    csrfPreventionSaltCache.put(salt, Boolean.TRUE);
                    if (modelAndView != null) {
                        System.out.println("Model and view not null and salt is added in modelAndView");
                        modelAndView.addObject("csrfPreventionSalt", salt);
                    }
                }
            } catch (Exception ex) {
                System.out.println(ex.getMessage());
                ex.printStackTrace();
            }
        }

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
                throws Exception {
            System.out.println("afterCompletion : ");
            if (ex != null) {
                System.out.println("exception : " + ex.getMessage());
                ex.printStackTrace();
            }
        }

        private List<String> ignoreUrl() {
            if(urlList == null) {
                urlList.add("/august/error");
                //add here your ignored url.
            }
            return urlList;
        }

        private void parameterValuesPrint(HttpServletRequest request, String err) {
            StringBuilder reqParamAndValue = new StringBuilder();
            Enumeration<?> params = request.getParameterNames();
            while (params.hasMoreElements()) {
                Object objOri = params.nextElement();
                String param = (String) objOri;
                String value = request.getParameter(param);
                reqParamAndValue = reqParamAndValue.append(param + "=" + value + ",");
            }
            System.out.println(CSRF_TAG + " " + err + "RequestedURL : " + request.getRequestURL());
        }
    }


以下是使用Spring上下文进行拦截器注册的代码:

package com.august.configuration;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.ViewResolver;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    import org.springframework.web.servlet.view.InternalResourceViewResolver;

    import com.august.security.CsrfSecurity;

    @Configuration
    @EnableWebMvc
    @ComponentScan(basePackages="com.august")
    public class SpringConfiguration extends WebMvcConfigurerAdapter  {

        @Bean
        public ViewResolver viewResolver() {
            InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
            //viewResolver.setViewClass(JstlView.class);
            viewResolver.setPrefix("/WEB-INF/views/");
            viewResolver.setSuffix(".jsp");
            return viewResolver;

        }

        @Bean
        public CsrfSecurity csrfSecurity() {
            return new CsrfSecurity();
        }
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new CsrfSecurity());
        }
    }

以下是我的控制器。

    package com.august.v1.appcontroller;

    import javax.servlet.http.HttpSession;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;


    @Controller
    public class HomeController {

        @Autowired 
        HttpSession httpSession;

        @RequestMapping("/")
        public String index(Model model) {
            httpSession.invalidate();
            System.out.println("Home page loaded");
            return "index";
        }
    }

以下是我的index.jsp JSP页面。

    <%@ page language="java" contentType="text/html; charset=ISO-8859-1"
        pageEncoding="ISO-8859-1" isELIgnored="false"%>
         //don't forget to add isELIgnored="false" on old(version) jsp page because of i 
         //have wasted 1 hour for this
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
    <title>ADS Home</title>
    </head>
    <body>
    <h1>${csrfPreventionSalt}</h1>
    <input type="hidden" name="csrfPreventionSalt" value=${csrfPreventionSalt}>
    </body>
    </html>


了解CSRF的相关信息 - CSRF解释

0

跨站点请求伪造利用用户的会话,因此,如果您没有会话,则不存在跨站点请求伪造。


7
虽然这个答案完全没有帮助,但从技术上讲是正确的。 - Florian Winter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接