vlambda博客
学习文章列表

CSRF漏洞的原理与防御

本文转载于SegmentFault社区


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网站关于当前用户的一些信息。


CSRF漏洞的原理与防御
CSRF漏洞的原理与防御


这个脚本让每个不同的登录用户打开,都会根据当前用户来展示关注数和粉丝数,这就足以说明可以获得目标网站的当前用户的信息,并能够代表用户发送请求。


这只是个无害的get请求,如果是post请求呢?



CSRF攻击



知道了原理,攻击就变得好理解了,接着上面的例子,


CSDN 肯定是做了防御了哈,我就不白费力气了。



CSRF防御



三种防御方式:


1. SameSit


禁止第三方网站使用本站Cookie。


这是后端在设置Cookie时候给SameSite的值设置为Strict或者Lax。


当设置Strict的时候代表第三方网站所有请求都不能使用本站的Cookie。


当设置Lax的时候代表只允许第三方网站的GET表单、<a>标签和<link>标签携带Cookie。


当设置None的时候代表和没设一样。


  
    
    
  

@Beanpublic 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() {
}}





点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流。

- END -