漏洞保护
9.1CSRF攻击与防御
9.1.1CSRF简介
CSRF(Cross-Site Request Forgery,跨站请求伪造),也可称为一键式攻击(one-click attack)。 CSRF攻击是一种挟持用户在当前已登录的浏览器上发送恶意请求的攻击方法。相对于XSS利用用户对指定网站的信任,CSRF则是利用网站对用户网页浏览器的信任。简单来说,CSRF是攻击者通过一些技术手段欺骗用户的浏览器,去访问一个用户曾经认证过的网站并执行恶意请求,例如发送邮件甚至财产操作。由于客户端(浏览器)已经在该网站上认证过,所以该网站会认为是真正的用户在操作而执行请求。 例如,假设javaboy现在登录了某银行的网站准备完成一项转账操作,相关链接为:https://bank.xxx.com/withdraw?account=javaboy&amount=1000&for=zhangsan 。假设javaboy没有注销登录该银行的网站,就在同一个浏览器新的选项卡中打开了一个危险网站,这个危险网站中有一幅图片:
<img src="https://bank.xxx.com/withdraw?account=javaboy&amount=1000&for=lisi"/>
一旦用户打开了这个网站,这个图片链接中的请求就会自动发送出去。由于是同一个浏览器并且用户尚未注销登录,所以该请求会自动携带上对应的有效的cookie信息,进而完成一次转账操作。
9.1.2CSRF攻击演示
转账接口,项目默认端口号为8080:
@RestController
public class HelloController {
@PostMapping("/withdraw")
public void withdraw() {
System.out.println("执行了一次转账操作");
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthenticaiton()
.withUser("javaboy")
.password("{noop}123")
.roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
}
危险网站,需要修改项目端口号为8081:
<form action="http://localhost:8080/withdraw" method="post">
<input type="hidden" value="javaboy" name="name">
<input type="hidden" value="10000" name="money">
<input type="submit" value="点我">
</form>
需要启动两个项目进行测试。输入http://localhost:8080 访问csrf-1,并完成登录操作,登录成功后,不要注销登录,继续打开一个新的选项卡访问csrf-2,单击表单按钮时发起请求,csrf-1项目的控制台会有日志打印,这就是一个跨站请求伪造。
9.1.3CSRF防御
CSRF攻击的根源在于浏览器默认的身份验证机制(自动携带当前网站的cookie信息),这种机制虽然可以保证请求时来自用户的某个浏览器,但是无法确保该请求是用户授权发送的。攻击者和用户发送的请求一模一样,这意味着没有办法去直接拒绝这里的某一个请求。如果能在合法请求中额外携带一个攻击者无法获取的参数,就可以成功区分出两种不同的请求,进而拒绝掉恶意请求。 Spring中提供了两种机制来防御CSRF攻击:
- 令牌同步模式。
- 在cookie上指定
SameSite 属性。
无论是哪种方式,前提都是请求方法幂等,即HTTP请求中的GET、HEAD、OPTIONS、TRACE方法不应该改变应用的状态。
令牌同步模式
目前主流的方案。 具体的操作方式就是在每一个HTTP请求中,除了默认自动携带的cookie参数外,再额外提供一个安全的、随机生成的字符串,称之为CSRF令牌。这个令牌由服务器生成,生成后在HttpSession 中保存一份。当前端请求到达后,将请求携带的令牌信息和服务端中保存的令牌进行对比,如果两者不相等,则拒绝掉该HTTP请求。 考虑到会有一些外部站点连接到自己的网站,所以要求请求是幂等的,这样对于GET、HEAD、OPTIONS、TRACE等方法就没有必要使用CSRF令牌了,强行使用可能会导致令牌泄露。
<form action="/hello" method="post">
<input type="hidden" th:value="${_csrf.token}" th:name="${_csrf.parameterName}">
<input type="submit" value="hello">
</form>
@Controller
public class HelloController {
@PostMapping("/hello")
@ResponseBody
public String hello() {
return "hello csrf!";
}
@GetMapping("/index.html")
public String index() {
return "index";
}
}
针对静态的AJAX请求,spring security也提供了相应的方案:即将CSRF令牌放在响应头cookie中,开发者自行从cookie中提取出令牌信息,然后再作为参数提交到服务器。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy")
.password("{noop}123")
.roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login.html")
.successHandler((req, resp, auth) -> {
resp.getWriter().write("login success");
})
.permitAll()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
<div>
<input type="text" id="username">
<input type="password" id="password">
<input type="button" value="登录" id="loginBtn">
</div>
<script>
$('#loginBtn').click(function() {
let _csrf = $.cookie('XSRF-TOKEN');
$.post('/login.html', {
username: $('#username').val(),
password: $('#password').val(),
_csrf: _csrf
}, function(data) {
alert(data);
});
});
</script>
CSRF攻击的根源在于浏览器默认的身份认证机制,即发送请求时会自动携带上网站的cookie,但是cookie的内容是什么黑客是不知道的,所以即使非法请求携带了含有CSRF令牌的cookie也没用,只有将CSRF令牌从cookie中解析出来,并放到请求头或者请求参数中,才有用。
SameSite
目前使用较少,后期有需求的话再来了解。
需要注意的问题
会话超时:CSRF令牌生成后,往往都保存在HttpSession 中,但是HttpSession 可能会因为超时而失效,导致前端请求传来的令牌无法得到验证,解决这一问题的方式:
- 最佳方案是在表单提交时,通过JS获取CSRF令牌,然后将获取到的令牌跟随表单一起提交。
- 当会话快要过期时,前端通过JS提醒用户刷新页面,以给会话续命。
- 将令牌存储在cookie中而不是
HttpSession 中。
登录和注销:为了保护用户的敏感信息,登录请求和注销请求需要注意CSRF攻击防护。
文件上传:这类请求比较特殊,因此需要额外注意。如果将CSRF放在请求体中,就会面临一个"鸡和蛋"的问题。服务端需要先验证CSRF令牌以确认请求是否合法,而这也意味着需要先读取请求体以获取CSRF令牌,这就陷入一个死循环了。 一般来说,将CSRF防御与multipart/form-data 一起使用,有两种不同的策略:
- 将CSRF令牌放在请求体中。
- 将CSRF令牌放在请求URL中。
将令牌放在请求体中,意味着任何人都可以向服务器上传临时文件,但是只有令牌验证通过的用户,才能真正提交一个文件,这也是目前推荐的方案,因为上传临时文件对服务器的影响可以忽略不计。如果不希望未经授权的用户上传临时文件,那么可以将令牌放在请求URL地址中,但是这种方式可能带来令牌泄露的风险。
9.1.4源码分析
CsrfToken
Spring security中提供了CsrfToken 接口用来描述令牌信息:
public interface CsrfToken extends Serializable {
String getHeaderName();
String getParameterName();
String getToken();
}
DefaultCsrfToken 是一个默认的实现类,该类为三个接口提供了对应的属性,属性值通过构造方法传入,再通过各自的get方法返回。 SaveOnAccessCsrfToken 是一个代理类,由于CsrfToken 只有两个实现类,所以正常来说SaveOnAccessCsrfToken 代理的就是DefaultCsrfToken 。代理类中主要是对getToken 方法做了改变,当调用getToken 方法时,才去执行令牌的保存操作,这样可以避免很多无用的操作。
CsrfTokenRepository
CsrfToken 的保存接口。
public interface CsrfTokenRepository {
CsrfToken generateToken(HttpServletRequest request);
void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
CsrfToken loadToken(HttpServletRequest request);
}
HttpSessionCsrfTokenRepository 是将CsrfToken 保存在HttpSession 中:
public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
if (token == null) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
}
else {
HttpSession session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
@Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
}
private String createNewToken() {
return UUID.randomUUID().toString();
}
}
CookieCsrfTokenRepository 则是将CsrfToken 保存在cookie中:
public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
public CookieCsrfTokenRepository() {
}
@Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
}
@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = (token != null) ? token.getToken() : "";
Cookie cookie = new Cookie(this.cookieName, tokenValue);
cookie.setSecure((this.secure != null) ? this.secure : request.isSecure());
cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request));
cookie.setMaxAge((token != null) ? this.cookieMaxAge : 0);
cookie.setHttpOnly(this.cookieHttpOnly);
if (StringUtils.hasLength(this.cookieDomain)) {
cookie.setDomain(this.cookieDomain);
}
response.addCookie(cookie);
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return null;
}
String token = cookie.getValue();
if (!StringUtils.hasLength(token)) {
return null;
}
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
public static CookieCsrfTokenRepository withHttpOnlyFalse() {
CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
result.setCookieHttpOnly(false);
return result;
}
}
CookieCsrfTokenRepository 可以通过两种方式获取其实例,第一种方式是直接新建一个实例,这种情况下生成的cookie中的HttpOnly 属性默认为true ,即前端不能通过JS操作cookie;第二种方式是调用静态方法withHttpOnlyFalse ,设置HttpOnly 属性默认为false ,即允许前端通过JS操作cookie。 LazyCsrfTokenRepository 是一个代理类,可以代理HttpSessionCsrfTokenRepository 或者CookieCsrfTokenRepository ,代理的目的是延迟保存生成的CsrfToken 。
public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
public LazyCsrfTokenRepository(CsrfTokenRepository delegate) {
this.delegate = delegate;
}
@Override
public CsrfToken generateToken(HttpServletRequest request) {
return wrap(request, this.delegate.generateToken(request));
}
@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
return this.delegate.loadToken(request);
}
private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
HttpServletResponse response = getResponse(request);
return new SaveOnAccessCsrfToken(this.delegate, request, response, token);
}
}
CsrfFilter
CsrfFilter 是spring security过滤器链中的一环,在过滤器中校验客户端传来的CSRF令牌是否有效。CsrfFilter 继承自OncePerRequestFilter ,所以对它来说最重要的方法是doFilterInternal :
public final class CsrfFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = (csrfToken == null);
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
this.logger.debug(
LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}
}
CsrfFilter 过滤器是由CsrfConfigurer 进行配置的,而CsrfConfigurer 则是在WebSecurityConfigurerAdapter#getHttp 方法中添加进HttpSecurity 中的。
CsrfAuthenticationStrategy
CsrfAuthenticationStrategy 实现了SessionAuthenticaitonStrategy 接口,默认也是由CompositeSessionAuthenticationStrategy 代理执行,在用户登录成功后触发执行。 主要用于在登录成功后,删除旧的CsrfToken 并生成一个新的CsrfToken 。
public final class CsrfAuthenticationStrategy implements SessionAuthenticationStrategy {
public CsrfAuthenticationStrategy(CsrfTokenRepository csrfTokenRepository) {
this.csrfTokenRepository = csrfTokenRepository;
}
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;
if (containsToken) {
this.csrfTokenRepository.saveToken(null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken);
request.setAttribute(newToken.getParameterName(), newToken);
}
}
}
9.2HTTP响应头处理
HTTP响应头中的许多属性都可以用来提高web安全。Spring security默认情况下,显式支持的HTTP响应头主要有如下几种:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
这些响应头都是在HeaderWriterFilter 中添加的,默认情况下,该过滤器就会添加到spring security过滤器链中,HeaderWriterFilter 是通过HeadersConfigurer 进行配置的。
public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<HeadersConfigurer<H>, H> {
@Override
public void configure(H http) {
HeaderWriterFilter headersFilter = createHeaderWriterFilter();
http.addFilter(headersFilter);
}
private HeaderWriterFilter createHeaderWriterFilter() {
List<HeaderWriter> writers = getHeaderWriters();
if (writers.isEmpty()) {
throw new IllegalStateException(
"Headers security is enabled, but no headers will be added. Either add headers or disable headers security");
}
HeaderWriterFilter headersFilter = new HeaderWriterFilter(writers);
headersFilter = postProcess(headersFilter);
return headersFilter;
}
private List<HeaderWriter> getHeaderWriters() {
List<HeaderWriter> writers = new ArrayList<>();
addIfNotNull(writers, this.contentTypeOptions.writer);
addIfNotNull(writers, this.xssProtection.writer);
addIfNotNull(writers, this.cacheControl.writer);
addIfNotNull(writers, this.hsts.writer);
addIfNotNull(writers, this.frameOptions.writer);
addIfNotNull(writers, this.hpkp.writer);
addIfNotNull(writers, this.contentSecurityPolicy.writer);
addIfNotNull(writers, this.referrerPolicy.writer);
addIfNotNull(writers, this.featurePolicy.writer);
addIfNotNull(writers, this.permissionsPolicy.writer);
writers.addAll(this.headerWriters);
return writers;
}
private <T> void addIfNotNull(List<T> values, T value) {
if (value != null) {
values.add(value);
}
}
}
9.2.1缓存控制
和缓存控制相关的响应头一共有三个:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Cache-Control
Cache-Control 是HTTP/1.1中引入的缓存字段,无论是请求头还是响应头都支持该字段。
no-store 表示不做任何缓存,每次请求都会从服务端完整地下载内容。no-cache 则表示缓存但是需要重新验证,这种情况下,数据虽然缓存在客户端,但是当需要使用该数据时,还是会向服务端发送请求,服务端则验证请求中所描述的缓存是否过期,如果没有过期,则返回304,客户端使用缓存;如果已经过期,则返回最新数据。max-age 则表示缓存的有效期,这个有效期并非一个时间戳,而是一个秒数,指从请求发起后多少秒内缓存有效。must-revalidate 表示当缓存在使用一个陈旧的资源时,必须先验证它的状态,已过期的将不被使用。
Pragma
Pragma 是HTTP/1.0中定义的响应头,作用类似于Cache-Control: no-cache ,但是并不能代理Cache-Control ,该字段主要用来兼容HTTP/1.0的客户端。
Expires
Expires 响应头指定了一个日期,即在指定日期之后,缓存过期。如果日期值为0的话,表示缓存已经过期。
可以看到,spring security默认就是不做任何缓存。但是需要注意,这个是针对经过spring security过滤器的请求,如果请求本身都没经过spring security的过滤器,那么该缓存的还是会缓存的。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello.html");
}
}
如果请求经过spring security过滤器,同时开发者又希望开启缓存功能,那么可以关闭关于缓存的默认配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.headers()
.cacheControl()
.disable()
.and()
.formLogin()
.and()
.csrf().disable();
}
}
9.2.2X-Content-Type-Options
要理解X-Content-Type-Options 响应头,得先了解MIME嗅探。 一般来说,浏览器通过响应头Content-Type 来确定响应包围类型,但是在早期浏览器中,为了提高用户体验,并不会严格根据Content-Type 的值来解析响应报文,当Content-Type 的值缺失,或者浏览器认为服务端给出了错误的Content-Type 值,此时就会对响应报文进行自我解析,即自动判断报文类型然后进行解析,在这个过程中就有可能触发XSS攻击。 X-Content-Type-Options 响应头相当于一个提示标志,被服务器用来提示客户端一定要遵循在Content-Type 中对MIME类型的设定,而不能对其进行修改。这就禁用了客户端的MIME类型嗅探行为,换言之,就是服务端告诉客户端其对于MIME类型的设置没有任何问题。 如果开发者不想禁用MIME嗅探,可以通过配置从响应头中移除:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.headers()
.contentTypeOptions()
.disable()
.and()
.formLogin()
.and()
.csrf().disable();
}
9.2.3Strict-Transport-Security
用来指定当前客户端只能通过HTTPS访问服务端,而不能通过HTTP访问。
Strict-Transport-Security: max-age=31536000; includeSubDomains
max-age :设置在浏览器收到这个请求后的多少秒的时间内,凡是访问这个域名下的请求都使用HTTPS请求。includeSubDomains :这个参数是可选的,如果被指定,表示第一条规则也适用于子域名。
这个响应头并非总是会添加,如果当前请求是HTTPS请求,这个请求才会添加,否则该请求头就不会添加,具体实现逻辑在HstsHeaderWriter#writeHeaders 方法中。
@Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (!this.requestMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Not injecting HSTS header since it did not match request to [%s]",
this.requestMatcher));
}
return;
}
if (!response.containsHeader(HSTS_HEADER_NAME)) {
response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue);
}
}
也可以手动配置Strict-Transport-Security :
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.httpStrictTransportSecurity()
.includeSubDomains(false)
.maxAgeInSeconds(3600);
}
}
9.2.4X-Frame-Options
用来告诉浏览器是否允许一个页面在<frame> 、<iframe> 、<embed> 或者<object> 中展现,通过该响应头可以确保网站没有被嵌入到其他站点里面,进而避免发生单击劫持。 X-Frame-Options 响应头有三种不同的取值:
deny (默认值):表示该页面不允许在frame 中展示,即便是在相同域名的页面中嵌套也不允许。sameorigin :表示该页面可以在相同域名页面的frame 中展示。allow-from uri :表示该页面可以在指定来源的frame 中展示。
如果项目需要,开发者也可以对此进行修改:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
.headers()
.frameOptions()
.sameOrigin();
}
}
单击劫持是一种视觉上的欺骗手段。攻击者将被劫持的网页放在一个iframe 标签中,设置该iframe 标签透明不可见,然后将标签覆盖在另一个网页上,最后诱使用户在该网页上进行操作,通过调整iframe 页面的位置,可以诱使用户恰好单击在iframe 页面的一些功能性按钮上。
9.2.5X-XSS-Protection
该响应头告诉浏览器,当检测到跨站脚本攻击(XSS)时,浏览器将停止加载页面,有四种不同的取值:
0 表示禁止XSS过滤。1 表示启用XSS过滤(通常浏览器是默认的)。如果检测到跨站脚本攻击,浏览器将清除页面(删除不安全的部分)。1;mode=block 表示启用XSS过滤。如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。1;report=<reporting-URI> 表示启用XSS过滤。如果检测到跨站脚本攻击,浏览器将清除页面,并使用CSP report-uri指令的功能发送违规报告(chrome支持)。
当然开发者也可以对此进行配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
.headers()
.xssProtection()
.block(false);
}
}
跨站脚本攻击(Cross-Site Scripting, XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的JS代码,而浏览器无法区分出来这是恶意的还是正常的代码。当被攻击者登录网站时,就会自动运行这些恶意代码,攻击者可以利用这些恶意代码去窃取cookie信息、监听用户行为以及修改DOM结构等。
其他一些安全相关的响应头需要手动配置。
9.2.6Content-Security-Policy
内容安全策略(Content Security Policy, CSP)是一个额外的安全层,用于检测并削弱某些特定类型的攻击,例如XSS和数据注入攻击等。 CSP相当于通过一个白名单明确告诉客户端,哪些外部资源可以加载和执行。例如:
Content-Security-Policy: default-src 'self';script-src 'self';
object-src 'none';style-src cdn.javaboy.org;img-src *;child-src https:
响应头含义如下:
default-src 'self' :默认情况下所有资源只能从当前域中加载。接下来细化的配置会覆盖default-src ,没有细化的选项则使用default-src 。script-src 'self' :表示脚本文件只能从当前域名加载。object-src 'none' :表示object 标签不加载任何资源。style-src cdn.javaboy.org 表示只加载来自cdn.javaboy.org 的样式表。img-src * :表示可以从任意地址加载图片。child-src https: :表示必须使用HTTPS来加载frame 。
其他选项可以自行搜索。
Spring security为Content-Security-Policy 提供了配置方法:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.contentSecurityPolicy("default-src 'self'; script-src 'self'; object-src 'none'; style-src cdn.javaboy.org; img-src *; child-src https:");
}
}
CSP还有一种报告模式——report-only 。在此模式下,CSP策略不是强制性的,如果出现违规行为,还是会继续加载相应的脚本或者样式表,但是会将违规行为报告给一个指定的URI地址:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.contentSecurityPolicy(contentSecurityPolicyConfig -> {
contentSecurityPolicyConfig.policyDirectives("default-src 'self'; script-src 'self'; object-src 'none';
style-src cdn.javaboy.org; img-src *; child-src https:; report-uri http://localhost:8081/report");
contentSecurityPolicyConfig.reportOnyly();
});
}
}
9.2.7Referrer-Policy
描述用户从哪里进入到当前网页。浏览器默认的取值:
Referrer Policy: no-referrer-when-downgrade
这个表示如果是从HTTPS网址链接到HTTP网址,就不发送Referer 字段,其他情况发送。开发者可以对此进行修改:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.referrerPolicy()
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.ORIGIN);
}
}
配置的取值有:
origin :表示总是发送源信息(源信息仅包含请求协议和域名,不包含其他路径信息,与之相对的是完整的URL)。no_referrer :表示从请求头中移除Referer 字段。same-origin :表示链接到同源地址时,发送文件源信息作为引用地址,否则不发送。strict-origin :表示从HTTPS链接到HTTP时不发送源信息,否则发送。origin-when-cross-origin :表示对于同源请求会发送完整的URL作为引用地址,但是对于非同源请求,则只发送源信息。strict-origin-when-cross-origin :表示对于同源的请求,会发送完整的URL作为引用地址;跨域时,如果是从HTTPS链接到HTTP,则不发送Referer 字段,否则发送文件的源信息。unsafe-uri :表示无论是同源请求还是非同源请求,都发送完整的URL(移除参数信息之后)作为引用地址。
9.2.8Feature-Policy
提供了一种可以在本页面或包含的iframe 上启动或禁止浏览器特性的机制(移动端开发使用较多)。例如,如果想要禁用震动和定位API:
Feature-Policy: vibrate 'none'; geolocation 'none'
Spring security配置如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.featurePolicy("vibrate 'none'; geolocation 'none'");
}
}
9.2.9Clear-Site-Data
一般用在注销登录响应头中,表示告诉浏览器清除当前网站相关的数据(cookie、cache、storage等)。可以通过具体的参数指定想要清除的数据,也可以通过"*"表示清除所有数据。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL)))
.and()
.csrf().disable();
}
}
9.3HTTP通信安全
HTTP通信安全主要从三个方面入手:
- 使用HTTPS代替HTTP。
Strict-Transport-Security 配置。- 代理服务器配置。
具体的配置可以后期来了解。
|