最近扫出了一个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: /*