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数据重新写入。
参考文章: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参数都会解析到!如果有任何问题可以联系本人,可以共同探讨!
-------------------------------------------
个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!