本文轉載於SegmentFault社區
作者:Mshu
CSRF 全稱:Cross Site Request Forgery,譯:跨站請求偽造
場景
點擊一個連結之後發現:帳號被盜,錢被轉走,或者莫名發表某些評論等一切自己不知情的操作。
CSRF是什麼
csrf 是一個可以發送http請求的腳本。可以偽裝受害者向網站發送請求,達到修改網站數據的目的。
原理
當你在瀏覽器上登錄某網站後,cookie會保存登錄的信息,這樣在繼續訪問的時候不用每次都登錄了,這個大家都知道。而CSRF就利用這個登陸態去發送惡意請求給後端。
為什麼腳本可以獲得目標網站的cookie呢?
只要是請求目標網站,瀏覽器會自動帶上該網站域名下面的cookie,看下面的腳本,可以證明惡意腳本可以獲得CSDN網站的登錄信息。
前提是你已經在瀏覽器上登錄了CSND網站。
< html> < head> < metacharset= "utf-8"/> < title> csrf demo title> head> < body> 您在CSDN上的粉絲數: < spanid= "fans_num"> span> 關注數: < spanid= "follow_num"> span> < > 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; }) > 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表單、標籤和標籤攜帶Cookie。
當設置None的時候代表和沒設一樣。
@BeanpublicCookieSerializer httpSessionIdResolver{ DefaultCookieSerializer cookieSerializer = newDefaultCookieSerializer; cookieSerializer.setCookieName( "JESSIONID"); cookieSerializer.setUseHttpOnlyCookie( true); cookieSerializer.setSameSite( "Lax"); cookieSerializer.setUseSecureCookie( true); returncookieSerializer; }
缺點:
目前只有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 ,放行。
/*** @authormashu * Date 2020/6/22 9:37*/@Slf4j @Component@WebFilter(urlPatterns = "/*", filterName = "verificationTokenFilter", deion = "用於校驗token") publicclassVerificationTokenFilterimplementsFilter{
List
@Overridepublicvoidinit(FilterConfig filterConfig)throwsServletException { }
@OverridepublicvoiddoFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)throwsIOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;//忽略不需要token的請求String serviceUrl = httpServletRequest.getServletPath;for( finalString 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);}
@Overridepublicvoiddestroy{
}}
文章來源: https://twgreatdaily.com/zh-mo/7mEQ8HIBiuFnsJQV-Fum.html