SpringCloud版本:2021.0.1     SpringBoot版本:2.6.3

系列文章

SpringCloud学习(一)----- Eureka搭建

SpringCloud学习(二)----- SpringBoot Admin搭建(与Eureka整合)

SpringCloud学习(三)----- Gatewayw网关搭建

SpringCloud学习(四)----- Gatewayw网关完善(限流)

SpringCloud学习(五)----- Gatewayw网关完善(Resilience4j断路器)

SpringCloud学习(六)----- Gatewayw网关完善(防止SQL注入)

SpringCloud学习(七)----- 使用Feign调用别的微服务的方法

SpringCloud学习(八)----- Gateway网关及其他微服务接入Swagger接口文档

参考文章:

Spring Cloud Gateway 实现XSS、SQL注入拦截 - 简书

高防IP:为什么一定要防护SQL注入_数据库

这一次要讲的是如何在Gateway网关里通过拦截器来防止不怀好意的人对我们的服务的sql注入。

一、什么是SQL注入?

        SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息 。

        他们将Web页面的表单域、原URL或数据包输入的参数修改拼接成SQL语句传递给Web服务器,由此传给数据库服务器以执行数据库命令。如果Web应用程序的开发人员不验证或过滤用户所输入的Cookie和数据内容就直接传输给数据库就可能导致这段SQL命令被执行,从而获取数据库权限。

二、原理

        首先,SQL注入能使黑客绕过认证机制,远程进入目标服务器中的数据库从而控制它。

        目前,大多数Web应用都使用SQL数据库来存放程序数据,而几乎所有的Web应用在后台都使用某种SQL数据库。

        和大多数语言一样,SQL语法允许数据库命令和用户数据混杂在一起;通常黑客们会访问有SQL注入漏洞的网站,寻找注入点;在找到漏洞之后,构造语句注入程序中,与程序里的SQL语句结合生成新的SQL语句;紧接着新的SQL语句被提交到新数据库中进行处理,数据库执行此段SQL语句后引发SQL注入攻击。      

SQL注入有两种形式。

        一是直接将代码插入到与SQL命令串联在一起并使得其以执行的用户输入变量。由于其直接与SQL语句捆绑,故也被称为直接注入式攻击法;

        二是一种间接的攻击方法,它将恶意代码注入要在表中存储或者作为原书据存储的字符串。在存储的字符串中会连接到一个动态的SQL命令中,以执行一些恶意的SQL代码。注入过程的工作方式是提前终止文本字符串,然后追加一个新的命令。

        以直接将代码插入到SQL命令串的攻击方式为例,在输入用户访问数据的时候,先用一个分号结束当前的语句,然后再插入一个恶意SQL语句即可。由于插入的命令可能在执行前追加其他字符串,因此攻击者常常用注释标记“一”来终止注入的字符串;执行时,系统会将此文本理解为语句注释,因此不执行编译后续文本。

  

三、导致SQL注入的原因:

        1、 再精致的网页都存在漏洞,而动态网页以及脚本编程学起来比较容易,这就相对导致了很多经验水平不够的程序员做动态网站,编写的代码存在一些漏洞,这些漏洞为攻击者提供了捷径。

        2、程序或系统对用户输入的参数不进行检查和过滤,没有对用户输入数据的合法性进行判断,或者程序中本身的变量处理不当,使应用程序存在安全隐患。

        3、因为 SQL 注入是从正常的 www端口访问,主要是针对 web 应用程序提交数据库查询请求的攻击,与正常的用户访问没有什么区别,所以能够轻易的绕过防火墙直接访问数据库,甚至能够获得数据库所在的服务器的访问权限。

        或者说,正因为SQL注入的成本低,因此企业常常会对自己的网站和后台数据库被轻而易举地入侵而烦恼,这也成为了云WAF、高防IP不同于高防服务器的特殊之处,也是高防IP价格比较昂贵的原因:能够防止网页篡改,抵御Web攻击。

四、如何抵御SQL注入

        一般来说,项目开发人员都会在拦截器中编写抵御SQL注入的方法,但这只适合一个项目的情况下,在微服务的情景下,每个项目都写这个功能显然是不现实的,当然,我们也可以把拦截器封装在一个包里,然后每个微服务的项目都引入这个包就可以了,不过这不是今天的主题,后续可以展开来讲,今天要说的是如何从网关处防止SQL注入,这种也是比较适合微服务的一个方法,因为不管哪个服务,我们都得从网关处进行访问,那么,如果在网关处建立抵御机制,那么就可以很好的对整个微服务架构进行监控及构建一个防御的体系。

        首先,我们需在Gateway网关服务里新建一个拦截器,没错,就是拦截器,万变不离其宗,就算是在网关拦截也是得先从写拦截器开始。

        新增SqLinjectionRuleUtils工具类,我们需要在该工具类中编写校验方法,一般来说,大部分的开发人员对sql注入拦截规则都是使用一个sql关键字匹配。

//定义sql注入关键字
String badStr = "'|and|exec|execute|insert|select|delete|update|count|drop|%|chr|mid|master|truncate|" +
                    "char|declare|sitename|net user|xp_cmdshell|;|or|+|,|like'|and|exec|execute|insert|create|drop|" +
                    "table|from|grant|use|group_concat|column_name|" +
                    "information_schema.columns|table_schema|union|where|select|delete|update|order|by|count|" +
                    "chr|mid|master|truncate|char|declare|or|;|--|,|like|//|/|%|#";
//过滤规则
for (String bad : badStrs) {
                if (value1.equalsIgnoreCase(bad)) {
                    value1 = "forbid";
                    mapjson.put(entry.getKey(),value1);
                    break;
                } else {
                    mapjson.put(entry.getKey(),entry.getValue());
                }
            }
        }

        但这种方法有个不足,就是容易误杀正常的业务,太过笼统,而且也容易漏,体验感太差 ,所以,我们要用的方法是sql正则匹配的方式,这种比单一的匹配sql关键字好,也不会误杀正常的业务,附正则表达式:

private static 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)";

sqLinjectionRuleUtils工具类完整代码:

package com.chenai.chenai_gatway.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;

public class SqLinjectionRuleUtils {
    private static final Logger logger = LoggerFactory.getLogger(SqLinjectionRuleUtils.class);
    private static 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 Pattern sqlPattern = Pattern.compile(badStrReg, Pattern.CASE_INSENSITIVE);//整体都忽略大小写


    /**
     * get请求sql注入校验
     *
     * @param value
     * @return
     */
    public static boolean getRequestSqlKeyWordsCheck(String value) throws UnsupportedEncodingException {

        //参数需要url编码
        //这里需要将参数转换为小写来处理
        //不改变原值
        //value示例 order=asc&pageNum=1&pageSize=100&parentId=0
        String lowerValue = URLDecoder.decode(value, "UTF-8").toLowerCase();

        //获取到请求中所有参数值-取每个key=value组合第一个等号后面的值
        return Stream.of(lowerValue.split("\\&"))
                .map(kp -> kp.substring(kp.indexOf("=") + 1))
                .parallel()
                .anyMatch(param -> {
                    if (sqlPattern.matcher(param).find()) {
                        logger.error("参数中包含不允许sql的关键词");
                        return true;
                    }
                    return false;
                });
    }


    /**
     * post请求sql注入校验
     *
     * @param value
     * @return
     */
    public static boolean postRequestSqlKeyWordsCheck(String value) {
        Object jsonObj = JSON.parse(value);
        if (jsonObj instanceof JSONObject) {
            JSONObject json = (JSONObject) jsonObj;
            Map<String, Object> map = json;

            //对post请求参数值进行sql注入检验
            return map.entrySet().stream().parallel().anyMatch(entry -> {

                //这里需要将参数转换为小写来处理
                String lowerValue = Optional.ofNullable(entry.getValue())
                        .map(Object::toString)
                        .map(String::toLowerCase)
                        .orElse("");


                if (sqlPattern.matcher(lowerValue).find()) {
                    logger.error("参数[{}]中包含不允许sql的关键词", lowerValue);
                    return true;
                }
                return false;
            });
        } else {
            JSONArray json = (JSONArray) jsonObj;
            List<Object> list = json;
            //对post请求参数值进行sql注入检验
            return list.stream().parallel().anyMatch(obj -> {

                //这里需要将参数转换为小写来处理
                String lowerValue = Optional.ofNullable(obj)
                        .map(Object::toString)
                        .map(String::toLowerCase)
                        .orElse("");


                if (sqlPattern.matcher(lowerValue).find()) {
                    logger.error("参数[{}]中包含不允许sql的关键词", lowerValue);
                    return true;
                }
                return false;
            });
        }


    }
}

        新建SqLinjectionFilter拦截器:


@Component
@RefreshScope
public class SqLinjectionFilter implements GlobalFilter, Ordered {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private String[] sqlinjectionHttpUrls = new String[0];

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // grab configuration from Config object
        logger.debug("----自定义防sql注入网关全局过滤器生效----");
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        HttpMethod method = serverHttpRequest.getMethod();
        String contentType = serverHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        URI uri = exchange.getRequest().getURI();

        //1.动态刷新 sql注入的过滤的路径
        String path = serverHttpRequest.getURI().getRawPath();
        String matchUrls[] = this.getSqlinjectionHttpUrls();

        if (AuthUtils.isMatchPath(path, matchUrls)) {
            logger.error("请求【{}】在sql注入过滤白名单中,直接放行", path);
            return chain.filter(exchange);
        }

        Boolean postFlag = (method == HttpMethod.POST || method == HttpMethod.PUT) &&
                (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_VALUE.equals(contentType));

        //过滤get请求
        if (method == HttpMethod.GET) {

            String rawQuery = uri.getRawQuery();
            if (StringUtils.isBlank(rawQuery)) {
                return chain.filter(exchange);
            }

            logger.debug("请求参数为:{}", rawQuery);
            // 执行sql注入校验清理
            boolean chkRet = false;
            try {
                chkRet = SqLinjectionRuleUtils.getRequestSqlKeyWordsCheck(rawQuery);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            //    如果存在sql注入,直接拦截请求
            if (chkRet) {
                logger.error("请求【" + uri.getRawPath() + uri.getRawQuery() + "】参数中包含不允许sql的关键词, 请求拒绝");
                return setUnauthorizedResponse(exchange);
            }
            //透传参数,不对参数做任何处理
            return chain.filter(exchange);
        }
        //post请求时,如果是文件上传之类的请求,不修改请求消息体
        else if (postFlag) {

            return DataBufferUtils.join(serverHttpRequest.getBody()).flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(
                    Optional.empty())
                    .flatMap(optional -> {
                        // 取出body中的参数
                        String bodyString = "";
                        if (optional.isPresent()) {
                            byte[] oldBytes = new byte[optional.get().readableByteCount()];
                            optional.get().read(oldBytes);
                            bodyString = new String(oldBytes, StandardCharsets.UTF_8);
                        }
                        HttpHeaders httpHeaders = serverHttpRequest.getHeaders();
 
                        logger.debug("{} - [{}] 请求参数:{}", method, uri.getPath(), bodyString);
                        boolean chkRet = false;
                        if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
                            //如果MediaType是json才执行json方式验证
                            chkRet = SqLinjectionRuleUtils.postRequestSqlKeyWordsCheck(bodyString);
                        } else {
                            //form表单方式,需要走get请求
                            try {
                                chkRet = SqLinjectionRuleUtils.getRequestSqlKeyWordsCheck(bodyString);
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                        }

                        //  如果存在sql注入,直接拦截请求
                        if (chkRet) {
                            logger.error("{} - [{}] 参数:{}, 包含不允许sql的关键词,请求拒绝", method, uri.getPath(), bodyString);
                            return setUnauthorizedResponse(exchange);
                        }

                        ServerHttpRequest newRequest = serverHttpRequest.mutate().uri(uri).build();

                        // 重新构造body
                        byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
                        DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
                        Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);

                        // 重新构造header
                        HttpHeaders headers = new HttpHeaders();
                        headers.putAll(httpHeaders);
                        // 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
                        int length = newBytes.length;
                        headers.remove(HttpHeaders.CONTENT_LENGTH);
                        headers.setContentLength(length);
                        headers.set(HttpHeaders.CONTENT_TYPE, contentType);
                        // 重写ServerHttpRequestDecorator,修改了body和header,重写getBody和getHeaders方法
                        newRequest = new ServerHttpRequestDecorator(newRequest) {
                            @Override
                            public Flux<DataBuffer> getBody() {
                                return bodyFlux;
                            }

                            @Override
                            public HttpHeaders getHeaders() {
                                return headers;
                            }
                        };

                        return chain.filter(exchange.mutate().request(newRequest).build());
                    });
        } else {
            return chain.filter(exchange);
        }

    }


    // 自定义过滤器执行的顺序,数值越大越靠后执行,越小就越先执行
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    /**
     * 设置403拦截状态
     */
    private Mono<Void> setUnauthorizedResponse(ServerWebExchange exchange) {
        return WebfluxResponseUtil.responseFailed(exchange, HttpStatus.FORBIDDEN.value(),
                "request is forbidden, SQL keywords are not allowed in the parameters.");
    }

    /**
     * 字节数组转DataBuffer
     *
     * @param bytes 字节数组
     * @return DataBuffer
     */
    private DataBuffer toDataBuffer(byte[] bytes) {
        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }

    public String[] getSqlinjectionHttpUrls() {
        return sqlinjectionHttpUrls;
    }

    public void setSqlinjectionHttpUrls(String[] sqlinjectionHttpUrls) {
        this.sqlinjectionHttpUrls = sqlinjectionHttpUrls;
    }

里面有些方法类可能会没有,大家可以去我参考的文章里面拿。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐