Spring Json String XSS 防禦策略客製化

Spring Json String Xss filtering Customization。

StdSerializer、StdDeserializer。

最近將手上的專案拿去掃 checkmarx,結果掃出幾個 XSS 弱點修補的問題,其實大致上就是沒有過濾網頁輸入以及輸出的字元是否含特殊符號。

1.為什麼要過濾

用以下的例子做說明,假設今天我們有一個頁面如下,需要輸入帳號密碼後登入,讓後端到資料庫查詢結果後回傳至前端。

而後端的查詢 SQL 長得像這樣。

String sqlStr = "select * from members where account='$account' and password='$password'";

代入我們前端輸入的帳號密碼之後,我們期待系統用這樣的 SQL 下去查詢,在帳號密碼都對的情況下查出結果。

select * from members where account=Ryuichi and password=123456

但如果輸入的部分改成像下面這樣,再送出。

後端的 SQL 就會變成。

select * from members where account='' or 1=1 -- and password=123456

這時問題就大條了,因為 or 1=1 的條件,我等於不用輸入任何帳號密碼都可以讓這 SQL 成立,藉此拿到撈出來的資料。

以上是使用者輸入的部分。

輸出的部分也會有類似的狀況,假設我從資料庫撈好字串"測試輸出"4個字要丟到前端頁面的 DIV 進行顯示,正常狀態下應該如下。

但如果資料庫的內容被動過手腳,改成<script>alert('駭入成功')</script>,而你又直接將它輸出的話就會變成像下面這樣。

被塞入一段可執行的 script,有心人士就可以做他想做的事情。

因此我們需要對特殊字元例如 <、>、'、"...等做 HTML 轉義字符的轉換,使其不可執行。

2.作法

其實網路上有很多不錯的做法,但我自己 google 後覺得這個做法最全面也最好理解,同時在客製化上也最彈性。

用繼承抽象類別 StdSerializer 和 StdDeserializer 的方式,將 Json 裡的 String 裡的特殊字元替換掉。 

首先先撰寫兩個輸入與輸出的類別分別繼承上面兩個抽象類別後,利用 Spring 的 HtmlUtils.htmlEscape 方法對 Json 每個 String 進行轉義。

import java.io.IOException;

import org.springframework.web.util.HtmlUtils;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

public class DefaultJsonSerializer extends StdSerializer<String> {
	
	public DefaultJsonSerializer() {
		this(null);
	}
	
	public DefaultJsonSerializer(Class<String> t) {
		super(t);
	}

	@Override
	public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
		String safe = HtmlUtils.htmlEscape(value, "utf-8");
		gen.writeString(safe);
	}
}
import java.io.IOException;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.util.HtmlUtils;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

public class DefaultJsonDeserializer extends StdDeserializer<String> {
	
	public DefaultJsonDeserializer() {
		this(null);
	}
	
	public DefaultJsonDeserializer(Class<String> t) {
		super(t);
	}
	
	@Override
	public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
		String value = p.getValueAsString();
		if(StringUtils.isEmpty(value)) {
			return value;
		} else {
			value = HtmlUtils.htmlEscape(value.toString(), "utf-8");
			return value;
		}
	}
}

接著在你的 Spring Webconfig 設置,新增一個 SimpleModule,並將實作的兩個轉義類別加入,利用 ObjectMapper 註冊,最後用 MappingJackson2HttpMessageConverter 加入。

@ComponentScan("com.spring.tku")
public class WebConfig extends WebMvcConfigurerAdapter {
	
	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		SimpleModule module = new SimpleModule();
		module.addDeserializer(String.class, new DefaultJsonDeserializer());//檢驗輸入參數
		module.addSerializer(String.class, new DefaultJsonSerializer());//檢驗輸出結果
		ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().build();
		
		mapper.registerModule(module);
		MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(mapper);
		converters.add(converter);
	}
}

我們用前面提到的例子測試一下。

xssExample.jsp。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<script src="${pageContext.request.contextPath}/static/js/jquery-ui/external/jquery/jquery.js"></script>
<script src="${pageContext.request.contextPath}/static/js/jquery-ui/jquery-ui.js"></script>
<script src="${pageContext.request.contextPath}/static/js/xssExample.js"></script>
</head>
<body>
帳號<input type="text" id="account" name="input" value=""/>
<br>
密碼<input type="text" id="password" name="password" value=""/>
<button id="button" onclick="xssInAndOut();" >執行</button>
<br>
<div id="getOutput"></div>
</body>
</html>

利用 Ajax 呼叫後端 Contorller。

這裡要注意,要讓 Ajax 與後端 Controller 可用 Json 的方式傳遞參數。

Ajax 的設定必須有以下幾行。

function xssInAndOut() {
	
	var account = $("#account").val();
	var password = $("#password").val();
	
	var xssObject = {
		'account' : account,
		'password' : password
	}
	
	$("#getOutput").val("");
	$.ajax({
		url:'/spring_ryuichi/xssInAndOut',
		type:"post",
		data : JSON.stringify(xssObject),
		dataType : "JSON",
		contentType:"application/json",
		success:function(data) {
			
			alert("success ");
			$( "#getOutput" ).append( data.output );
			$("#input").val("");
		}
	});
}
@Controller
@ComponentScan("com.spring.tku")
public class TkuController {
@RequestMapping(value = "/xssInAndOut", method = RequestMethod.POST)
	public @ResponseBody XssObject xssInAndOut(@RequestBody XssObject xssObject){
		
		//模擬使用sqlstatement 輸入參數組SQL
		String sqlStr = "select * from members where account='$account' and password='$password'";
		sqlStr = sqlStr.replace("'$account'", xssObject.getAccount());
		sqlStr = sqlStr.replace("'$password'", xssObject.getPassword());
		
		System.out.println("sqlStr: " + sqlStr);
		
		//模擬從資料庫輸出特殊字元到頁面
		xssObject.setOutput("<script>alert('駭入成功')</script>");
		
		return xssObject;
	}
}

我們使用的 XssObject 物件代碼如下。

public class XssObject {
	
	public String account;
	public String password;
	public String output;
	
	public String getAccount() {
		return account;
	}
	public void setAccount(String account) {
		this.account = account;
	}
	public String getOutput() {
		return output;
	}
	public void setOutput(String output) {
		this.output = output;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
}

執行過程與結果如下:

輸入參數檢查。

輸出參數檢查。