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;
}
}
執行過程與結果如下: