title: 跨站請求偽造防御
date: 2017-08-14 16:22:41
categories: 網絡安全
tags: csrf
開發相關
- jdk1.8
- springmvc
- 掃描軟件 Acunetix WVS
背景
最近安全問題越來越多,公司軟件也面臨出海,剛開始公司軟件大部分部在公安內網,安全問題沒有太多重視。最近買了安全公司的掃描軟件,一掃掃出很多安全問題,其中有一個是跨站請求偽造問題。
常見的攻擊模式
GET請求利用
案例二
使用GET請求方式的利用是最簡單的一種利用方式,其隱患的來源主要是由於在開發系統的時候沒有按照HTTP動詞的正確使用方式來使用造成的。對於GET請求來說,它所發起的請求應該是只讀的,不允許對網站的任何內容進行修改。
但是事實上並不是如此,很多網站在開發的時候,研發人員錯誤的認為GET/POST的使用區別僅僅是在於發送請求的數據是在Body中還是在請求地址中,以及請求內容的大小不同。對於一些危險的操作比如刪除文章,用戶授權等允許使用GET方式發送請求,在請求參數中加上文章或者用戶的ID,這樣就造成了只要請求地址被調用,數據就會產生修改。
現在假設攻擊者(用戶ID=121)想將自己的身份添加為網站的管理員,前提是網站使用get方法修改資源,他在網站A上面發了一個帖子,里面包含一張圖片,其地址為 http://a.com/user/grant_super_user/121
<img src="http://a.com/user/grant_super_user/121" />
設想管理員看到這個帖子的時候(之所以必須是網站管理員看到,因為增加管理員可能只有admin有這個權限),這個圖片肯定會自動加載顯示的。於是在管理員不知情的情況下,一個賦予用戶管理員權限的操作已經悄悄的以他的身份執行了。這時候攻擊者121就獲取到了網站的管理員權限。
POST請求利用
相對於GET方式的利用,POST方式的利用更加復雜一些,難度也大了一些。攻擊者需要建立一個釣魚網站,偽造一個表單來發送POST請求。例如首先通過xss拿到你的cookie並鏈接到釣魚網站,此時你剛剛登錄被攻擊網站,session還在,通過誘導你進入釣魚網站,然后用吸引眼球的文字和圖片,誘導你點擊一個按鈕,向被攻擊網站發起post。
<script>
$(function() {
$('#CSRF_forCSRFm').trigger('submit');
});
</script>
<form action="http://a.com/user/grant_super_user" id="CSRF_form" method="post">
<input name="uid" value="121" type="hidden">
</form>
只要想辦法實現用戶訪問的時候自動提交表單就可以了。
網上的方案
- 驗證 HTTP Referer 字段
- 在請求地址中添加 token 並驗證
- 在 HTTP 頭中自定義屬性並驗證
三種方案優缺點
驗證 HTTP Referer 字段
- 容易篡改,低版本瀏覽器不安全,隱私軟件可能禁用referer
- 公司軟件沒有域名,無法針對域名進行過濾
在請求地址/參數中添加token並驗證
- 改動接口較多
- 難以保證token本身安全性
在HTTP 頭中自定義屬性並驗證
- 改動很大 幾乎要重寫網站
自己的方案
由於寫代碼的時候post get使用比較規范,get方法用於獲取資源,沒有對后台數據修改的,所以掃描的問題絕大多數在post表單提交
針對掃描軟件的掃描報告,跨站腳本攻擊都是由form表單提交的接口發起
為了能減少對代碼的修改,采用注解加攔截器的方式,這樣做的好處是通過攔截器和注解,對特定方法做出一致性處理,減少代碼量
具體流程圖如下

關鍵代碼CSRFInterceptor
public class CSRFInterceptor implements HandlerInterceptor {
private final Logger logger = LoggerFactory.getLogger(CSRFInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
VerifyCSRFToken annotation = method.getAnnotation(VerifyCSRFToken.class);
if (annotation != null && annotation.verify()) {
String token = (String)request.getParameter(Constants.CSRF_TOKEN);
if (token == null || !token.equals(request.getSession(true).getAttribute(Constants.CSRF_TOKEN))) {
RestResult restResult = new RestResult(false, "CSRF Token Verify fail");
restResult.putError("message" ,"CSRF Token 驗證失敗,請刷新頁面");
response.setContentType("text/html; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().append(JsonUtlis.getJsonUtlis().object2String(restResult));
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
關鍵代碼VerifyCSRFToken
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyCSRFToken {
boolean verify() default true;
}
關鍵代碼Controller
public class ScriptController {
private static final ObjectMapper mapper = new ObjectMapper();
private Logger logger = LoggerFactory.getLogger(ScriptController.class);
@GetMapping(value = "")
public String indexView(HttpServletRequest request, Model model) {
model.addAttribute(Constants.CSRF_TOKEN, request.getSession(false).getAttribute(Constants.CSRF_TOKEN));
return "script/script";
}
@PostMapping(value = "/upload")
@VerifyCSRFToken
public @ResponseBody RestResult uploadScript(@RequestParam("file")MultipartFile file, String type, HttpServletResponse response){
}
參考資料
https://www.ibm.com/developerworks/cn/web/1102_niugang_csrf/
https://segmentfault.com/a/1190000008505616
http://blog.csdn.net/jrn1012/article/details/52750883
http://book.51cto.com/art/201102/245185.htm
