10.6 Cross Site Request Forgery (CSRF)

10.6.1 CSRF Attacks

在讨论Spring Security 如何保护应用程序免受CSRF攻击之前,我们将解释什么是CSRF攻击。让我们看一个具体的例子,以便更好地理解。

假设您的银行网站提供了一个表单,允许将资金从当前登录的用户转移到另一个银行帐户。例如,HTTP请求可能如下所示:

POST /transfer HTTP/1.1

Host: bank.example.com

Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly

Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

现在假设你通过了银行网站的身份验证,然后,不注销,访问一个邪恶的网站。邪恶网站包含一个HTML页面,其格式如下:

<form action="https://bank.example.com/transfer" method="post">

<input type="hidden" name="amount" value="100.00"/>

<input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>

<input type="hidden" name="account" value="evilsAccountNumber"/>

<input type="submit" value="Win Money!"/>

</form>

你喜欢赢钱,所以你点击提交按钮。在此过程中,您无意中将100美元转移给了恶意用户。这是因为,虽然邪恶的网站看不到您的cookie,但与您的银行相关联的cookie仍然随请求一起发送。

最糟糕的是,整个过程可以使用JavaScript实现自动化。这意味着你甚至不需要点击按钮。那么,我们如何保护自己免受这种攻击呢?

10.6.2 Synchronizer Token Pattern

问题是,来自银行网站的HTTP请求和来自邪恶网站的请求完全相同。这意味着无法拒绝来自邪恶网站的请求只允许来自银行网站的请求。为了防止CSRF攻击,我们需要确保邪恶站点无法提供请求中的某些内容。

一种解决方案是使用  Synchronizer Token Pattern。这个解决方案是为了确保除了会话cookie之外,每个请求都需要一个随机生成的令牌作为HTTP参数。提交请求时,服务器必须查找参数的预期值,并将其与请求中的实际值进行比较。如果值不匹配,则请求应失败。

我们可以放宽期望,只需要更新状态的每个HTTP请求的令牌。这可以安全地完成,因为同源策略确保邪恶的站点无法读取响应。此外,我们不希望在HTTP GET中包含随机令牌,因为这可能导致令牌泄漏。

让我们看看我们的示例将如何改变。假设随机生成的令牌存在于名为 _csrf 的HTTP参数中。例如,转账请求如下:

POST /transfer HTTP/1.1

Host: bank.example.com

Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly

Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

您会注意到我们使用随机值添加了_csrf参数。现在邪恶网站无法猜测CSRF参数的正确值(必须在邪恶网站上明确提供),当服务器将实际令牌与预期令牌进行比较时,传输将失败。

10.6.3 When to use CSRF protection

什么时候应该使用CSRF保护?我们的建议是对正常用户可以通过浏览器处理的任何请求使用CSRF保护。如果您只创建一个非浏览器客户端使用的服务,那么您可能希望禁用CSRF保护。

CSRF protection and JSON

一个常见的问题是,“我需要保护JavaScript发出的JSON请求吗?”简而言之,这要看情况而定。但是,您必须非常小心,因为存在可能影响JSON请求的CSRF漏洞。例如,恶意用户可以使用以下表单使用JSON创建CSRF:

<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">

<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>

<input type="submit"

    value="Win Money!"/>

</form>

这将生成以下JSON结构

"amount": 100,

"routingNumber": "evilsRoutingNumber",

"account": "evilsAccountNumber",

"ignore_me": "=test"

}

如果应用程序没有验证 Content-Type,那么它将暴露于此漏洞中。根据设置的不同,验证 Content-Type 的SpringMVC应用程序仍然可以通过更新url后缀以“.json”结尾来利用,如下所示:

<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">

<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>

<input type="submit"

    value="Win Money!"/>

</form>

CSRF and Stateless Browser Applications

如果我的应用程序是无状态的呢?这并不一定意味着你受到了保护。事实上,如果用户对于给定的请求不需要在Web浏览器中执行任何操作,那么他们可能仍然容易受到CSRF攻击。    

例如,假设一个应用程序使用一个自定义cookie,该cookie包含它内部的所有状态来进行身份验证,而不是JSessionID。当进行CSRF攻击时,定制cookie将与请求一起发送,发送方式与上一个示例中发送JSessionID cookie的方式相同。

使用 basic 认证的用户也容易受到CSRF攻击,因为浏览器将自动在任何请求中包含用户名密码,其方式与我们上一个示例中发送JSessionID cookie的方式相同。

10.6.4 Using Spring Security CSRF Protection

那么,使用Spring Security保护我们的站点免受CSRF攻击所必需的步骤是什么?使用Spring Security的CSRF保护的步骤概述如下:    

\bullet  Use proper HTTP verbs

\bullet  Configure CSRF Protection

\bullet  Include the CSRF Token

Use proper HTTP verbs 使用正确的HTTP动词

防止CSRF攻击的第一步是确保您的网站使用正确的HTTP动词。具体来说,在Spring Security的CSRF支持可以使用之前,您需要确保您的应用程序正在使用  PATCH、POST、PUT和/或 DELETE 来修改状态。

这不是Spring Security支持的限制,而是正确预防CSRF的一般要求。原因是在HTTP GET中包含私有信息会导致信息泄漏。请参阅  RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s ,以获取有关使用POST而不是GET获取敏感信息的一般指导。

Configure CSRF Protection 配置CSRF保护

下一步是在你的应用程序中引入Spring Security 的CSRF保护。一些框架通过使用户的会话无效来处理无效的CSRF令牌,但这会导致自身的问题。相反,默认情况下,Spring Security的CSRF保护将导致HTTP 403访问被拒绝。这可以通过配置 AccessDeniedHandler 以不同方式处理 InvalidCsrfTokenException 来定制。

从Spring Security 4.0开始,CSRF保护默认通过XML配置启用。如果要禁用CSRF保护,可以在下面看到相应的XML配置。

<http>

        <!-- ... -->

        <csrfdisabled="true"/>

</http>

CSRF保护在使用Java配置时默认启用。如果您想禁用CSRF,可以看下面相应的Java配置。有关CSRF保护的其他自定义配置,请参阅csrf()的javadoc。

@EnableWebSecurity

public class WebSecurityConfig extends

WebSecurityConfigurerAdapter {

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http

            .csrf().disable();

    }

}

Include the CSRF Token

Form Submissions 表单提交

最后一步是确保在所有 PATCH, POST, PUT 和 DELETE  方法中都包含CSRF令牌。一种方法是使用 _csrf 请求属性获取当前 CsrfToken 。使用JSP执行此操作的示例如下所示:

<c:url var="logoutUrl" value="/logout"/>

<form action="${logoutUrl}"  method="post">

        <input type="submit" value="Log out" />

        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

</form>

一种更简单的方法是使用SpringSecurityJSP标记库中的 the csrfInput tag 标记。

如果您使用的是spring mvc<form:form> tag或 Thymeleaf 2.1+,并且使用的是 @enableWebSecurity,则会自动为你包含 CsrfToken  (使用 CsrfRequestDataValueProcessor )。

Ajax and JSON Requests

如果你正在使用JSON,那么就不可能在HTTP参数中提交CSRF令牌。相反,您可以在HTTP头中提交令牌。典型的模式是在 <meta> 中包含CSRF令牌。带有JSP的示例如下所示:

<html>

<head>

    <meta name="_csrf" content="${_csrf.token}"/>

    <!-- default header name is X-CSRF-TOKEN -->

    <meta name="_csrf_header" content="${_csrf.headerName}"/>

    <!-- ... -->

</head>

<!-- ... -->

你可以使用Spring Security JSP标记库中更简单的  csrfMetaTags tag,而不是手动创建meta标记。

然后可以在所有Ajax请求中包含该令牌。如果您使用的是jquery,则可以通过以下方法完成:

$(function () {

    var token = $("meta[name='_csrf']").attr("content");

    var header = $("meta[name='_csrf_header']").attr("content");

    $(document).ajaxSend(function(e, xhr, options) {

        xhr.setRequestHeader(header, token);

    });

});

作为jquery的替代方案,我们建议使用cujojs的rest.js。js模块为以RESTful方式处理HTTP请求和响应提供了高级支持。核心功能是通过将拦截器链接到客户机来根据需要添加行为,从而将HTTP客户机上下文化的能力。

var client = rest.chain(csrf, {

token: $("meta[name='_csrf']").attr("content"),

name: $("meta[name='_csrf_header']").attr("content")

});

配置的客户端可以与应用程序的任何组件共享,这些组件需要向受CSRF保护的资源发出请求。rest.js和jquery之间的一个显著区别是,只有使用配置的客户端进行的请求才会包含CSRF令牌,而jquery中的所有请求都将包含该令牌。确定接收令牌的请求范围的能力有助于防止CSRF令牌泄漏给第三方。有关rest.js的更多信息,请参阅rest.js参考文档。

CookieCsrfTokenRepository

在某些情况下,用户可能希望在cookie中保留 CsrfToken 。默认情况下,CookieCsrfTokenRepository 将在cookie中写入名为 XSRF-TOKEN 的 token,并从名为 X-XSRF-TOKEN 的头或http参数 _csrf 读取该 token。这些默认值来自AngularJS

可以使用以下方法在XML中配置 CookieCsrfTokenRepository 

<http>

    <!-- ... -->

    <csrf token-repository-ref="tokenRepository"/>

</http>

<b:bean id="tokenRepository"

    class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"

    p:cookieHttpOnly="false"/>

示例显式设置 cookieHttpOnly=false。这是允许JavaScript(即AngularJS)读取它所必需的。如果不需要直接使用javascript读取cookie,建议省略  cookieHttpOnly=false 以提高安全性。

10.6.5 CSRF Caveats

在实施CSRF时有一些注意事项。

Timeouts

一个问题是预期的CSRF令牌存储在 HttpSession 中,因此一旦 HttpSession 过期,配置的 AccessDeniedHandler 将收到一个无效的 InvalidCsrfTokenException。如果您使用的是默认的 AccessDeniedHandler,浏览器将得到一个HTTP403并显示一条错误消息。

有人可能会问,为什么默认情况下预期的 CsrfToken  没有存储在cookie中。这是因为存在已知的漏洞,其中头(即指定cookie)可以由另一个域设置。这也是RubyonRails在存在头X-requested-with时不再跳过CSRF检查的原因。有关如何执行该漏洞攻击的详细信息,请参阅此webappsec.org线程。另一个缺点是,通过删除状态(即超时),您将失去在令牌受到威胁时强制终止令牌的能力。

缓解活动用户超时的一个简单方法是使用一些JavaScript让用户知道他们的会话即将到期。用户可以单击按钮继续并刷新会话。

或者,指定一个自定义的accessdeniedHandler允许您以任何方式处理invalidCSRFtokenException。有关如何自定义AccessDeniedHandler 的示例,请参阅所提供的XML和Java配置的链接。

最后,可以将应用程序配置为使用不会过期的 CookicsrftokenRepository。如前所述,这并不像使用会话那样安全,但在许多情况下,这已经足够好了。

Logging In

为了防止伪造登录请求,登录表单也应防止CSRF攻击。由于 CsrfToken 存储在 HttpSession 中,这意味着一旦访问CsrfToken 属性,就会创建 HttpSession 。虽然这在一个RESTful/无状态的体系结构中听起来很糟糕,但事实是状态对于实现真正的安全是必要的。没有状态,一旦token被破坏我们就无能为力。实际上,CSRF令牌的大小非常小,对我们的体系结构的影响应该可以忽略不计。

保护登录表单的常见技术是在表单提交之前使用javascript函数获取有效的CSRF令牌。通过这样做,无需考虑会话超时(在上一节中讨论),因为会话是在表单提交之前创建的(假设没有配置 CookieCsrfTokenRepository),因此用户可以留在登录页面上,并在需要时提交用户名/密码。为了实现这一点,您可以利用Spring Security 提供的CsrfTokenArgumentResolver ,并像这里描述的那样公开一个端点。

Logging Out

启动了CSRF,将更新 LogoutFilter 只能使用 HTTP POST(不启用CSRF时,GET请求也是可以正常退出的,因为其不需要CSRF token)。这可以确保注销需要CSRF令牌,并且恶意用户无法强制注销您的用户。

一种方法是使用表单注销。如果你真的想要一个链接,你可以使用javascript让链接执行一个POST(也就是说,可能在一个隐藏的表单上)。对于禁用了javascript的浏览器,您可以让链接把用户带到一个注销的确认页面(POST提交)。

如果您真的想在注销时使用HTTP GET,可以这样做,但请记住,通常不建议这样做。例如,下面的Java配置将执行注销,URL  /logout 是用任何HTTP方法请求的:

@EnableWebSecurity

public class WebSecurityConfig extends

WebSecurityConfigurerAdapter {

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http

            .logout()

                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));

    }

}

Multipart (file upload)

multipart/form-data 使用CSRF保护有两个选项。每个选项都有其权衡。

\bullet  Placing MultipartFilter before Spring Security

\bullet  Include CSRF token in action

在将Spring Security的CSRF保护与多部分文件上传集成之前,请确保您可以先上传而不需要CSRF保护。有关将多部分表单与Spring一起使用的更多信息,可以在 17.10 Spring’s multipart (file upload) support 章节和 MultipartFilter javadoc 中找到。

Placing MultipartFilter before Spring Security

第一个选项是确保在Spring Security 过滤器之前指定 MultipartFilter 。在Spring安全过滤器之前指定 MultipartFilter  意味着没有调用 MultipartFilter  的授权,这意味着任何人都可以在服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件上载对大多数服务器都会产生不可忽略的影响。

为了确保在使用Java配置的Spring Security 过滤器之前指定 MultipartFilter ,用户可以覆盖beforeSpringSecurityFilterChain ,如下所示:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override

    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {

        insertFilters(servletContext, new MultipartFilter());

    }

}

为了确保 MultipartFilter 在使用xml配置的spring Security 过滤器之前被指定,用户可以确保  MultipartFilter <filter mapping> 元素放在web.xml中的 springSecurityFilterChain 之前,如下所示:

<filter>

    <filter-name>MultipartFilter</filter-name>

    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>

</filter>

<filter>

    <filter-name>springSecurityFilterChain</filter-name>

    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

</filter>

<filter-mapping>

    <filter-name>MultipartFilter</filter-name>

    <url-pattern>/*</url-pattern>

</filter-mapping>

<filter-mapping>

    <filter-name>springSecurityFilterChain</filter-name>

    <url-pattern>/*</url-pattern>

</filter-mapping>

Include CSRF token in action

如果不允许未经授权的用户上传临时文件,则可以将 MultipartFilter 放在spring Security 过滤器之后,并将 CSRF 作为查询参数包含在表单的action属性中。下面是一个带有JSP的示例

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

这种方法的缺点是可以泄漏查询参数。一般来说,最好的做法是将敏感数据放在主体或头中,以确保不会泄漏。其他信息可在 RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s.中查找。

HiddenHttpMethodFilter

HiddenHttpMethodFilter 应该放在Spring Security过滤器之前。一般来说,这是正确的,但在防止CSRF攻击时,它可能会有额外的影响。

请注意,HiddenHttpMethodFilter 只重写HTTP 的POST方法,因此这实际上不太可能导致任何实际问题。但是,最好还是将其放在Spring Security过滤器之前。

10.6.6 Overriding Defaults

SpringSecurity的目标是提供保护您的用户免受攻击的默认值。这并不意味着您必须接受它的所有默认值。

例如,您可以提供一个自定义的 CsrfTokenRepository 来覆盖 CSRFtoken 的存储方式。为了保证在分布式系统中可以使用,用Redis来存储是比较常见的实现方式。

您还可以指定一个自定义的 RequestMatcher 来确定哪些请求受CSRF保护(也就是说,您可能不关心logout是否安全)。简而言之,如果Spring Security的CSRF保护行为不完全符合您的要求,那么您可以自定义该行为。有关如何使用XML和java配置来进行这些自定义的详细信息,详见the section called “<csrf>”章节。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容