共计 3531 个字符,预计需要花费 9 分钟才能阅读完成。
CSRF 全称:Cross Site Request Forgery
,译:跨站请求伪造
场景
点击一个链接之后发现:账号被盗,钱被转走,或者莫名发表某些评论等一切自己不知情的操作。
CSRF 是什么
csrf 是一个可以发送 http 请求的脚本。可以伪装受害者向网站发送请求, 达到修改网站数据的目的。
原理
当你在浏览器上登录某网站后,cookie 会保存登录的信息,这样在继续访问的时候不用每次都登录了,这个大家都知道。而 CSRF 就利用这个登陆态去发送恶意请求给后端。
为什么脚本可以获得目标网站的 cookie 呢?
只要是请求目标网站,浏览器会自动带上该网站域名下面的 cookie, 看下面的脚本, 可以证明恶意脚本可以获得 CSDN 网站的登录信息。
前提是你已经在浏览器上登录了 CSND 网站。
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>csrf demo</title>
</head>
<body>
您在 CSDN 上的
粉丝数:<span id="fans_num"></span>
关注数:<span id="follow_num"></span>
<script>
fetch('https://me.csdn.net/api/relation/get', {credentials: 'include'}).then(res => res.json())
.then(
res => {document.getElementById('fans_num').innerText = res.data.fans_num;
document.getElementById('follow_num').innerText = res.data.follow_num;
})
</script>
</body>
</html>
保证 CSDN 的登录状态,用浏览器打开这个 html 文件,可以看到这个脚本已经获得了我在 csdn 上的用户信息。以及寒酸的粉丝数量!
F12 打开选择应用程序一栏左边 Cookie 还有来自 csdn 网站关于当前用户的一些信息。
这个脚本让每个不同的登录用户打开,都会根据当前用户来展示关注数和粉丝数,
这就足以说明可以获得目标网站的当前用户的信息,并能够代表用户发送请求。
这只是个无害的 get 请求,如果是 post 请求呢?
CSRF 攻击
知道了原理,攻击就变得好理解了,接着上面的例子,
我把请求地址改成评论本篇文章的 url, 参数为“这篇文章写得 6
”,
在没有 CSRF 防御的情况下,我发表一个评论如:脱单秘笈:
,后面附上这个脚本的链接,只要有用户点了链接,就会以他的名义给本篇文章发评论“这篇文章写得 6
”。
CSDN 肯定是做了防御了哈,我就不白费力气了。
CSRF 防御
三种防御方式:
1. SameSit,
禁止第三方网站使用本站 Cookie。
这是后端在设置 Cookie 时候给 SameSite
的值设置为 Strict
或者 Lax
。
当设置 Strict
的时候代表第三方网站所有请求都不能使用本站的 Cookie。
当设置 Lax
的时候代表只允许第三方网站的 GET
表单、<a>
标签和 <link>
标签携带 Cookie。
当设置 None
的时候代表和没设一样。
@Bean
public CookieSerializer httpSessionIdResolver(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setCookieName("JESSIONID");
cookieSerializer.setUseHttpOnlyCookie(true);
cookieSerializer.setSameSite("Lax");
cookieSerializer.setUseSecureCookie(true);
return cookieSerializer;
}
缺点:
目前只有 chrome 浏览器支持 ……..
2. referer
referer
代表着请求的来源,不可以伪造。
后端写个过滤器检查请求的 headers
中的 referer
,检验是不是本网站的请求。
题外话:referer
和 origin
的区别,只有 post 请求会携带 origin 请求头,而 referer 不论何种情况下都带。referer
正确的拼写 应该是 referrer
,HTTP 的标准制定者们将错就错,不打算改了
缺点:
浏览器可以关闭 referer……….
3. token
最普遍的一种防御方法,后端生成一个 token 放在 session 中并发给前端,前端发送请求时携带这个 token,后端通过校验这个 token 和 session 中的 token 是否一致判断是否是本网站的请求。
具体实现:
用户登录输入账号密码,请求登录接口,后端在用户登录信息正确的情况下将 token 放到 session 中,并返回 token 给前端,前端把 token 存放在 localstory 中,之后再发送请求都会将 token 放到 header 中。
后端写个过滤器,拦截 POST 请求,注意忽略掉不需要 token 的请求,比如登录接口,获取 token 的接口,免得还没有获取 token 就检验 token。
校验原则: session 中的 token 和前端 header 中的 token 一致的 post,放行。
/**
* @author mashu
* Date 2020/6/22 9:37
*/
@Slf4j
@Component
@WebFilter(urlPatterns = "/*", filterName = "verificationTokenFilter", description = "用于校验 token")
public class VerificationTokenFilter implements Filter {List<String> ignorePathList = ImmutableList.of("/demo/login","/demo/getToken");
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
// 忽略不需要 token 的请求
String serviceUrl = httpServletRequest.getServletPath();
for (final String ignorePath : ignorePathList) {if (serviceUrl.contains(ignorePath)) {filterChain.doFilter(servletRequest, servletResponse);
return;
}
}
String method = httpServletRequest.getMethod();
if ("POST".equals(method)) {String tokenSession = (String)httpServletRequest.getSession().getAttribute("token");
String token = httpServletRequest.getHeader("token");
if (null != token && null != tokenSession && tokenSession.equals(token)) {filterChain.doFilter(servletRequest, servletResponse);
return;
} else {log.error("验证 token 失败!" + tokenSession + "!=" + token);
httpServletResponse.sendError(403);
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {}
}