CSRF 令牌生成
Posted
技术标签:
【中文标题】CSRF 令牌生成【英文标题】:CSRF token generation 【发布时间】:2010-12-20 19:43:02 【问题描述】:这是一个关于生成 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 上。
这是一种幼稚的方法吗?我错过了为什么这不起作用的一些原因吗?
谢谢
【问题讨论】:
建议的解决方案容易受到重放攻击。相同的令牌和组合键将无限期地起作用。 好点,@Matthew。但是,当令牌由服务器生成,但用户没有访问我们的服务器并且它是由具有相同 sessionId+hash 的黑客完成时,我们如何防止这种情况呢?或者这是不可能的(不比较 ip-address/useragent 等?) 【参考方案1】:在 CSRF 令牌的帮助下,我们可以确定传入的请求是经过身份验证的(知道用户不是黑客)
请注意,我需要以下方法,但即使在 *** 上,谷歌也无法帮助我,我没有得到下面提到的代码,但在收集了 *** 答案后,我度过了愉快的一天。所以它对于进一步搜索/特别适合初学者很有用
我在下面描述了带有 Spring Interceptor 的 Spring MVC
注意 - 我已经使用谷歌缓存将盐存储在缓存中以进行重新验证
下面的依赖需要添加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 explanation
【讨论】:
【参考方案2】:CSRF 令牌有多种实现方式。关键是这个 csrf 令牌是在客户端还是服务器端生成的。因为这两种场景的实现发生了巨大变化,令牌的熵也发生了巨大变化。
对于服务器端,SecureRandom 是首选方式,但如果您希望在识别任何用户之前生成 CSRF 令牌,window.crypto 提供了此功能,您可以在其中生成 unguessable enough 字符串用于 CSRF 令牌。
【讨论】:
【参考方案3】:我想说你的方法有效,因为CSRF攻击是攻击者利用受害者的浏览器来伪造登录状态,他们为什么可以这样做?因为在大多数服务器端,会话检查是基于 cookie 中的 SessionID,而 cookie 是一条数据,会自动附加到发送到服务器的 HTTP 请求。
因此,防御CSRF有两个关键因素
-
生成挑战令牌,并要求客户端以非 cookie 的方式将其传递给服务器,URL 参数或 POST 形式都可以。
保持令牌安全,就像您对 SessionID 所做的一样,例如,使用 SSL。
我推荐阅读CSRF Prevention Cheat Sheet
【讨论】:
【参考方案4】:试试base64_encode(openssl_random_pseudo_bytes(16))
。
https://github.com/codeguy/php-the-right-way/issues/272#issuecomment-18688498 我将它用于https://gist.github.com/mikaelz/5668195 中的表单示例
【讨论】:
【参考方案5】:我认为基于 HMAC 进行散列的最佳想法,即通过以下顺序的某个密码进行散列加密:用户名+用户 ID+时间戳。每个请求的哈希值必须不同,如果您不想在攻击中简单地重放哈希值,时间戳必须是不同的。
【讨论】:
【参考方案6】:您只需要在 URL/表单和 cookie 中使用相同的“令牌”。这意味着您可以让您的页面通过 javascript 将令牌 cookie 设置为它想要的任何值(最好是一些随机值),然后在发送到您的服务器的所有请求中传递相同的值(作为 URI ?param 或 form-场地)。无需让您的服务器生成 cookie。
只要我们相信浏览器不允许来自某个域的页面编辑/读取其他域的 cookie,这是安全的,并且这在今天被认为是相当安全的。
让您的服务器生成令牌将假定此令牌可以安全地传输到您的浏览器,而不会被任何 CSRF 尝试拾取(为什么要冒险?)。虽然您可以将更多逻辑放入服务器生成的令牌中,但为了防止 CSRF,没有必要。
(如果我错了,请告诉我)
【讨论】:
【参考方案7】:CSRF 令牌旨在防止(无意的)数据修改,这通常与 POST 请求一起应用。
因此,您必须为每个更改数据的请求(GET 或 POST 请求)包含 CSRF 令牌。
我的问题是关于 没有时生成令牌 要使用的唯一用户数据。没有会话 可用,cookie 不是 选项,IP地址等 自然不可靠。
然后只需为每个访问者创建一个唯一的用户 ID。 在 cookie 或 URL 中包含该 id(如果 cookie 被禁用)。
编辑:
考虑以下事件:
您已登录您的 facebook 帐户,然后进入某个任意网站。
在那个网站上有一个您提交的表单,它告诉您的浏览器向您的 Facebook 帐户发送一个 POST 请求。
该 POST 请求可能会更改您的密码或添加评论等,因为 facebook 应用程序将您识别为注册和登录用户。 (除非有另一种阻塞机制,比如 CAPTCHA)
【讨论】:
将令牌的一部分添加到 URL 中,并在表单中包含另一半意味着根本没有保护。 当然,“在 cookie 或 URL 中包含该 id(如果 cookie 被禁用)。” - 你说要把 id 放在 URL 中,这根本不安全。 在禁用 cookie 时,我找不到更好的方法来存储会话 ID。 啊你的意思是把会话ID放在URL中,然后在会话中保留一半的CSRF令牌?当您谈论唯一用户 ID 时,我认为您的意思是用于生成令牌的唯一值。道歉。【参考方案8】:有什么理由不能将要散列的字符串也包含在请求中吗?
CSRF 令牌有两个部分。嵌入在表单中的令牌,以及其他地方的相应令牌,无论是在 cookie 中、存储在会话中还是在其他地方。这种在别处的使用会阻止页面自包含。
如果您在请求中包含要散列的字符串,则请求是自包含的,因此复制表单是攻击者需要做的所有事情,因为他们拥有令牌的两个部分,因此没有保护。
即使将它放在表单 URL 中也意味着它是自包含的,攻击者只需复制表单和提交 URL。
【讨论】:
不,你没有。一半可以保留在会话中,也可以通过 cookie 丢弃。它根本不必存储在服务器上,通常它是基于 cookie 的,因此您不必依赖启用会话。 根据 OWASP,这只是一个名为“双重提交 cookie”owasp.org/index.php/… 的缓解 你能解释一下这个“如果你在请求中包含要散列的字符串,那么请求是自包含的,所以复制表单是攻击者需要做的所有事情,因为他们有两个部分令牌,因此没有保护。”?【参考方案9】:CSRF 利用用户的会话,因此,如果您没有会话,则没有 CSRF。
【讨论】:
虽然这个答案一点用都没有,但在技术上是正确的。以上是关于CSRF 令牌生成的主要内容,如果未能解决你的问题,请参考以下文章
csrf 令牌会在 laravel 中的每个请求上自动重新生成,这会导致生产服务器上的 csrf 令牌不匹配