request請求的body中的引數(json物件)只能取出一次,引數丟失問題的解決方式(防sql注入過濾器的應用)
在專案即將上線的滲透測試報告中檢測出了sql注入的問題,關於這個問題的解決方案,最初的思路是寫一個全域性的過濾器,對所有請求的引數進行過濾攔截,如果存在和sql注入相關的特殊字元則攔截掉,具體細節展開以下討論!
(當然要提供一個白名單,白名單裡的請求不給予過濾)
首先提供以下白名單code.properties
# 鑑權碼 # IDAM鑑權(多個以逗號分隔) authcode=32j42i3 # 防sql注入請求白名單 sqlverify=/ryjh/mappingGroup/updateInfo,\ /author/Logon/loginConfigCheck,\ /author/Logon/login,\ /author/SAuUser/resetPwd,\ /author/SAuUser/addUser,\ /swagger-resources/configuration/ui,\ /swagger-resources,\ /doc.html
第一版的過濾器如下
/** * @author FanJiangFeng * @version 1.0.0 * @ClassName SqlFilter.java * @Description 防止Sql注入過濾器,校驗引數 * @createTime 2021年01月05日 17:08:00 */ @Component @WebFilter(value = "/") public class SqlFilter implements Filter { //Sql注入配置檔案白名單絕對路徑 @Value("${auth.authCodeUrl}") private String url; private boolean verify(String uri) throws IOException { Properties properties=new Properties(); InputStream inputStream=new FileInputStream(new File(url)); properties.load(inputStream); Map<String,String> codeMap=(Map)properties; String whiteDoc=codeMap.get("sqlverify"); String[] strings = whiteDoc.split(","); boolean over=false; for(String s:strings){ if(s.equals(uri)){ over=true; break; } } return over; } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request=(HttpServletRequest)servletRequest; String contentType = request.getContentType(); String requestURI = request.getRequestURI(); boolean verify = verify(requestURI); if(verify){ filterChain.doFilter(servletRequest,servletResponse); return; } //application/x-www-form-urlencoded Map<String, String[]> parameterMap = request.getParameterMap(); for(Map.Entry<String,String[]> entry:parameterMap.entrySet()){ // String strings = entry.getKey(); //校驗引數名是否合法 // boolean isTrue = verifySql(strings); // if(!isTrue){ // return; // } //校驗引數值是否合法 String[] value = entry.getValue(); for(String s:value){ //校驗引數值是否合法 boolean b = verifySql(s); if(!b){ return; } } } filterChain.doFilter(servletRequest,servletResponse); return; } @Override public void destroy() { } /** * 校驗引數非法字元 */ public boolean verifySql(String parameter){ if(parameter.contains("'")){ //' 單引號 return false; }else if(parameter.contains("\"")){ //" 雙引號 return false; }else if(parameter.contains("\\'")){//' 反斜槓單引號 return false; }else if(parameter.contains("\\\"")){//" 反斜槓雙引號 return false; }else if(parameter.contains("(")||parameter.contains(")")||parameter.contains(";")){//括號和分號 return false; }else if(parameter.contains("--")||parameter.contains("+")){//雙減號 加號 return false; }else if(parameter.toLowerCase().contains("select")||parameter.toLowerCase().contains("update") ||parameter.toLowerCase().contains("delete")||parameter.toLowerCase().contains("drop") ||parameter.toLowerCase().contains("updatexml")||parameter.toLowerCase().contains("concat")){ return false; } return true; } }
第一個版本的不足:
它只能解析content-type為application/x-www-form-urlencoded的請求攜帶的引數
由Map<String, String[]> parameterMap = request.getParameterMap()的方式進行獲取
但是它解析不了content-type型別為application/json格式的引數 ,上面那種方式已經獲取不到了,所以要重新改版。
我是如何跳坑的?
剛開始我新加了一個方法,傳入request物件,然後從request物件中拿到json字串格式的引數,通過對字串進行轉換校驗等處理,然後達到目的效果,但是我發現,處理之後,雖然過濾器放開了這個請求,當請求來到controller時,引數消失了?
這是因為,request請求中的body引數只可以拿出來一次,拿出來就沒有了!
解決方案
需要一個類繼承HttpServletRequestWrapper,該類繼承了ServletRequestWrapper並實現了HttpServletRequest,
因此它可作為request在FilterChain中傳遞。
該類需要重寫getReader和getInputStream兩個方法,並在返回時將讀出的body資料重新寫入。
參考文章:
新建BodyReaderRequestWrapper類
public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {
private final String body;
public String getBody() {
return body;
}
/**
* 取出請求體body中的引數(建立物件時執行)
* @param request
*/
public BodyReaderRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
StringBuilder sb = new StringBuilder();
InputStream ins = request.getInputStream();
BufferedReader isr = null;
try{
if(ins != null){
isr = new BufferedReader(new InputStreamReader(ins));
char[] charBuffer = new char[128];
int readCount = 0;
while((readCount = isr.read(charBuffer)) != -1){
sb.append(charBuffer,0,readCount);
}
}else{
sb.append("");
}
}catch (IOException e){
throw e;
}finally {
if(isr != null) {
isr.close();
}
}
sb.toString();
body = sb.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletIns = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayIns.read();
}
};
return servletIns;
}
}
filter過濾器更改
在dofilter方法中建立BodyReaderRequestWrapper物件,並繼續傳遞。
BodyReaderRequestWrapper wrapper=null;
if("application/json".equals(contentType)){
wrapper=new BodyReaderRequestWrapper(request);
......
if(wrapper==null){
filterChain.doFilter(servletRequest,servletResponse);
}else{
filterChain.doFilter(wrapper,servletResponse);
}
既然可以獲取到json物件的字串資訊了,那麼開始寫對json的校驗過程
對json格式引數遞迴解析
討論:json格式的引數種類很多,比如
{
"id":"test",
"name":"test"
}
[
{
"id":"test",
"name":"test"
}
{
"id":"test",
"name":"test"
}
]
{
"id":"test",
"name":[
{
"id":"test",
"name":"test"
}
{
"id":"test",
"name":"test"
}
]
}
以及更多,所以這裡採用遞迴解析的方式
過濾器的最終版本
@Component
@WebFilter(value = "/")
public class SqlFilter implements Filter {
//Sql注入配置檔案白名單絕對路徑
@Value("${auth.authCodeUrl}")
private String url;
private boolean verify(String uri) throws IOException {
Properties properties=new Properties();
InputStream inputStream=new FileInputStream(new File(url));
properties.load(inputStream);
Map<String,String> codeMap=(Map)properties;
String whiteDoc=codeMap.get("sqlverify");
String[] strings = whiteDoc.split(",");
boolean over=false;
for(String s:strings){
if(s.equals(uri)){
over=true;
break;
}
}
return over;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request=(HttpServletRequest)servletRequest;
String contentType = request.getContentType();
String requestURI = request.getRequestURI();
boolean verify = verify(requestURI);
if(verify){
filterChain.doFilter(servletRequest,servletResponse);
return;
}
BodyReaderRequestWrapper wrapper=null;
if("application/json".equals(contentType)){
wrapper=new BodyReaderRequestWrapper(request);
String requestPostStr = wrapper.getBody();
if (requestPostStr.startsWith("{")) {
//解析json物件
boolean b = resolveJSONObjectObj(requestPostStr);
if(!b)return;
}else if (requestPostStr.startsWith("[")) {
//把資料轉換成json陣列
JSONArray jsonArray = JSONArray.parseArray(requestPostStr);
jsonArray.forEach(json -> {
//解析json物件
boolean b = resolveJSONObjectObj(json.toString());
if(!b)return;
});
}
}else{
//application/x-www-form-urlencoded
Map<String, String[]> parameterMap = request.getParameterMap();
for(Map.Entry<String,String[]> entry:parameterMap.entrySet()){
// String strings = entry.getKey();
//校驗引數名是否合法
// boolean isTrue = verifySql(strings);
// if(!isTrue){
// return;
// }
//校驗引數值是否合法
String[] value = entry.getValue();
for(String s:value){
//校驗引數值是否合法
boolean b = verifySql(s);
if(!b){
return;
}
}
}
}
if(wrapper==null){
filterChain.doFilter(servletRequest,servletResponse);
}else{
filterChain.doFilter(wrapper,servletResponse);
}
return;
}
/**
* 對JSONObject物件進行遞迴引數解析
*
* @param requestPostStr
* @return
*/
private boolean resolveJSONObjectObj(String requestPostStr) {
boolean isover=true;
// 建立需要處理的json物件
JSONObject jsonObject = JSONObject.parseObject(requestPostStr);
// 獲取所有的引數key
Set<String> keys = jsonObject.keySet();
if (keys.size() > 0) {
for (String key : keys) {
//獲取引數名稱
String value = null;
if (jsonObject.get(key) != null) {
value = String.valueOf(jsonObject.get(key));
//當value為陣列時
if(value.startsWith("[")){
//把資料轉換成json陣列
JSONArray jsonArray = JSONArray.parseArray(value);
for(int i=0;i<jsonArray.size();i++){
//解析json物件
boolean b = resolveJSONObjectObj(jsonArray.get(i).toString());
if(!b){
isover=false;
break;
}
}
}else if(value.startsWith("{")){
boolean b = resolveJSONObjectObj(value);
if(!b){
isover=false;
break;
}
}else{
//校驗引數值是否合法
boolean b = verifySql(value);
if(!b){
isover=false;
break;
}
}
}
}
}
return isover;
}
@Override
public void destroy() {
}
/**
* 校驗引數非法字元
*/
public boolean verifySql(String parameter){
if(parameter.contains("'")){ //' 單引號
return false;
}else if(parameter.contains("\"")){ //" 雙引號
return false;
}else if(parameter.contains("\\'")){//' 反斜槓單引號
return false;
}else if(parameter.contains("\\\"")){//" 反斜槓雙引號
return false;
}else if(parameter.contains("(")||parameter.contains(")")||parameter.contains(";")){//括號和分號
return false;
}else if(parameter.contains("--")||parameter.contains("+")){//雙減號 加號
return false;
}else if(parameter.toLowerCase().contains("select")||parameter.toLowerCase().contains("update")
||parameter.toLowerCase().contains("delete")||parameter.toLowerCase().contains("drop")
||parameter.toLowerCase().contains("updatexml")||parameter.toLowerCase().contains("concat")){
return false;
}
return true;
}
}
這樣,什麼格式的json引數都會解析到!如果有任何問題可以聯絡本人,可以共同探討!