在項目即將上線的滲透測試報告中檢測出了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數據重新寫入。
參考文章:https://my.oschina.net/u/4335633/blog/4252883
新建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參數都會解析到!如果有任何問題可以聯系本人,可以共同探討!