最近掃出了一個SQL注入安全漏洞,用戶的非法輸入可能導致數據泄露、數據篡改甚至系統崩潰,為了有效防范 SQL 注入攻擊,除了在代碼層面使用參數化查詢和預編譯語句外,還可以通過實現一個Filter來過濾掉潛在的危險輸入。本文將介紹如何基于Filter接口實現SQL 注入過濾器
SQL注入攔截的原理很簡單,用請求參數匹配SQL關鍵字,匹配上了就說明請求存在非法字符不予放行
SqlLnjectionFilter SQL注入過濾器
package com.largescreen.common.filter;import com.fasterxml.jackson.databind.ObjectMapper;
import com.largescreen.common.enums.HttpMethod;
import com.largescreen.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** sql注入過濾器*/
@Slf4j
public class SqlLnjectionFilter implements Filter {/*** 白名單*/public List<String> excludes = new ArrayList<>();private static final AntPathMatcher matcher = new AntPathMatcher();private static final String badStrReg = "\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";/*** 整體都忽略大小寫*/private static final Pattern sqlPattern = Pattern.compile(badStrReg, Pattern.CASE_INSENSITIVE);@Overridepublic void init(FilterConfig filterConfig) throws ServletException {String tempExcludes = filterConfig.getInitParameter("excludes");if (StringUtils.isNotEmpty(tempExcludes)){String[] urls = tempExcludes.split(",");for (String url : urls){excludes.add(url);}}}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;String path = request.getServletPath();if(matchAny(excludes,path)){filterChain.doFilter(request, response);return;}// 從request中獲取當前請求中所有的參數名稱String sql = StringUtils.EMPTY;Map<String, String[]> parameterMap = request.getParameterMap();for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {String[] values = entry.getValue();for (int i = 0; i < values.length; i++) {sql += values[i];}}if (sqlValidate(sql)) {errorResp(response);} else {// 校驗post請求String contentType = request.getContentType();Boolean existSql = false;if (HttpMethod.POST.matches(request.getMethod())) {BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));String bodyString = IOUtils.toString(reader);if(StringUtils.isNotBlank(bodyString)){if(StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE)){existSql = sqlValidate(bodyString);}else if (StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {existSql = sqlValidate(bodyString);} else if (StringUtils.startsWithIgnoreCase(contentType, MediaType.MULTIPART_FORM_DATA_VALUE)) {existSql = sqlValidate(bodyString);}}// 如果存在sql注入,直接攔截請求if (existSql) {errorResp(response);return;};}}filterChain.doFilter(request, response);}@Overridepublic void destroy() {}/*** 非法請求響應* @param response* @throws IOException*/private void errorResp(HttpServletResponse response) throws IOException {response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());response.setContentType("application/json; charset=utf-8");response.setCharacterEncoding("UTF-8");Map result = new HashMap();result.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());result.put("msg","非法請求");ObjectMapper mapper = new ObjectMapper();String str = mapper.writeValueAsString(result);ServletOutputStream outputStream = response.getOutputStream();outputStream.write(new String(str.getBytes(),"utf-8").getBytes());outputStream.flush();}/*** 判斷輸入的字符串是否包含SQL注入** @param str 輸入的字符串* @return 如果輸入的字符串包含SQL注入,返回 true,否則返回 false。*/public static boolean sqlValidate(String str) {str = str.toLowerCase();Matcher matcher = sqlPattern.matcher(str);if (matcher.find()) {log.error("SqlInjectionFilter 參數[{}]中包含不允許sql的關鍵詞", str);return true;}return false;}/*** 匹配多個路徑** @param patterns 路徑模式列表* @param path 需要匹配的實際路徑* @return 匹配的路徑模式(如果匹配成功),否則返回 null*/public static boolean matchAny(List<String> patterns, String path) {for (String pattern : patterns) {if (matcher.match(pattern, path)) {return true;}}return false;}}
上面的過濾器除了會校驗請求地址中拼接的參數,還會校驗post請求體中的參數,但直接讀請求體的數據是有問題的,http請求的數據流只能讀取一次,在過濾器中讀取請求體后serlvlet就會拿不到數據,所以得增強request請求確保數據能重復讀取
RepeatedlyRequestWrapper? request可重復讀實現類
package com.largescreen.common.filter;import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import com.largescreen.common.utils.http.HttpHelper;
import com.largescreen.common.constant.Constants;/*** 構建可重復讀取inputStream的request* */
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{private final byte[] body;public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException{super(request);request.setCharacterEncoding(Constants.UTF8);response.setCharacterEncoding(Constants.UTF8);body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8);}@Overridepublic BufferedReader getReader() throws IOException{return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException{final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream(){@Overridepublic int read() throws IOException{return bais.read();}@Overridepublic int available() throws IOException{return body.length;}@Overridepublic boolean isFinished(){return false;}@Overridepublic boolean isReady(){return false;}@Overridepublic void setReadListener(ReadListener readListener){}};}
}
添加一個新的過濾器RepeatableFilter,在doFilter方法中對request進行增強,只要這個過濾器執行順序足夠靠前,后續的過濾器就都能重復讀參數了
RepeatableFilter 可重復讀過濾器
package com.largescreen.common.filter;import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;import com.largescreen.common.enums.HttpMethod;
import com.largescreen.common.utils.StringUtils;
import org.springframework.http.MediaType;/*** Repeatable 過濾器* */
public class RepeatableFilter implements Filter
{@Overridepublic void init(FilterConfig filterConfig) throws ServletException{}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException{ServletRequest requestWrapper = null;if (request instanceof HttpServletRequest&& HttpMethod.POST.matches(((HttpServletRequest) request).getMethod())){requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);}if (null == requestWrapper){chain.doFilter(request, response);}else{chain.doFilter(requestWrapper, response);}}@Overridepublic void destroy(){}
}
將上面兩個過濾器注冊到過濾鏈中
FilterConfig 過濾器配置
package com.largescreen.framework.config;import java.util.HashMap;
import java.util.Map;
import javax.servlet.DispatcherType;import com.largescreen.common.filter.SqlLnjectionFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.largescreen.common.filter.RepeatableFilter;
import com.largescreen.common.filter.XssFilter;
import com.largescreen.common.utils.StringUtils;/*** Filter配置**/
@Configuration
public class FilterConfig
{@Value("${sql.excludes}")private String sqlExcludes;@Value("${sql.urlPatterns}")private String sqlUrlPatterns;@Bean@ConditionalOnProperty(value = "sql.enabled", havingValue = "true")public FilterRegistrationBean sqlFilterRegistration(){FilterRegistrationBean registration = new FilterRegistrationBean();registration.setDispatcherTypes(DispatcherType.REQUEST);registration.setFilter(new SqlLnjectionFilter());registration.addUrlPatterns(StringUtils.split(sqlUrlPatterns, ","));registration.setName("sqlFilter");registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE + 10);Map<String, String> initParameters = new HashMap<>();initParameters.put("excludes", sqlExcludes);registration.setInitParameters(initParameters);return registration;}@SuppressWarnings({ "rawtypes", "unchecked" })@Beanpublic FilterRegistrationBean someFilterRegistration(){FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(new RepeatableFilter());registration.addUrlPatterns("/*");registration.setName("repeatableFilter");registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);return registration;}}
在配置文件中加入SQL過濾的白名單等信息
application.yml
# 防止sql注入
sql:# 過濾開關enabled: true# 排除鏈接(多個用逗號分隔)excludes: /system/*# 匹配鏈接urlPatterns: /*