SpringCloud微服务-实现XSS、SQL注入拦截

SQL注入是常见的系统安全问题之一,用户通过特定方式向系统发送SQL脚本,可直接自定义操作系统数据库,如果系统没有对SQL注入进行拦截,那么用户甚至可以直接对数据库进行增删改查等操作。

XSS全称为Cross Site Script跨站点脚本攻击,和SQL注入类似,都是通过特定方式向系统发送攻击脚本,对系统进行控制和侵害。SQL注入主要以攻击数据库来达到攻击系统的目的,而XSS则是以恶意执行前端脚本来攻击系统。

项目框架中使用mybatis/mybatis-plus数据持久层框架,在使用过程中,已有规避SQL注入的规则和使用方法。但是在实际开发过程中,由于各种原因,开发人员对持久层框架的掌握水平不同,有些特殊业务情况必须从前台传入SQL脚本。这时就需要对系统进行加固,防止特殊情况下引起的系统风险。

在微服务架构下,我们考虑如何实现SQL注入/XSS攻击拦截时,肯定不会在每个微服务都实现一遍SQL注入/XSS攻击拦截。根据我们微服务系统的设计,所有的请求都会经过Gateway网关,所以在实现时就可以参照前面的日志拦截器来实现。在接收到一个请求时,通过拦截器解析请求参数,判断是否有SQL注入/XSS攻击参数,如果有,那么返回异常即可。

我们前面在对微服务Gateway进行自定义扩展时,增加了Gateway插件功能。我们会根据系统需求开发各种Gateway功能扩展插件,并且可以根据系统配置文件来启用/禁用这些插件。下面我们就将防止SQL注入/XSS攻击拦截器作为一个Gateway插件来开发和配置

处理方法 :
新增SqlInjectionFilter 过滤器和XssInjectionFilter过滤器,分别用于解析请求参数并对参数进行判断是否存在SQL注入/XSS攻脚本。此处有公共判断方法,通过配置文件来读取请求的过滤配置,因为不是多有的请求都会引发SQL注入和XSS攻击,如果无差别的全部拦截和请求,那么势必影响到系统的性能。

1.首先新建 CacheBodyGlobalFilter.java

这个过滤器解决body不能重复读的问题(在低版本的spring-cloud不需要这个过滤器),为后续的XssRequestGlobalFilter重写请求body做准备。

package com.zhdj.gateway.filter;

import com.zhdj.gateway.utils.WebFluxUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 全局缓存获取body请求数据(解决流不能重复读取问题)
 *
 * @author Lion Li
 */
@Component
public class GlobalCacheRequestFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 只缓存json类型请求
        if (!WebFluxUtils.isJsonRequest(exchange)) {
            return chain.filter(exchange);
        }
        return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> {
            if (serverHttpRequest == exchange.getRequest()) {
                return chain.filter(exchange);
            }
            return chain.filter(exchange.mutate().request(serverHttpRequest).build());
        });
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

2.XSS跨站脚本配置

package com.zhdj.gateway.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * XSS跨站脚本配置
 *
 * @author ruoyi
 */
@Data
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.xss")
public class XssProperties {
    /**
     * Xss开关
     */
    private Boolean enabled;

    /**
     * 排除路径
     */
    private List<String> excludeUrls = new ArrayList<>();

}

3.Sql注入配置

package com.zhdj.gateway.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * Sql注入
 *
 * @author ruoyi
 */
@Data
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.sql")
public class SqlProperties {
    /**
     * Sql开关
     */
    private Boolean enabled;

    /**
     * 排除路径
     */
    private List<String> excludeUrls = new ArrayList<>();

}

4.跨站脚本过滤器

package com.zhdj.gateway.filter;

import cn.hutool.http.HtmlUtil;
import com.zhdj.common.core.utils.StringUtils;
import com.zhdj.gateway.config.properties.XssProperties;
import com.zhdj.gateway.utils.WebFluxUtils;
import io.netty.buffer.ByteBufAllocator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.*;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * 跨站脚本过滤器
 *
 * @author ruoyi
 */
@Component
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
public class XssFilter implements GlobalFilter, Ordered {
    // 跨站脚本的 xss 配置,nacos自行添加
    @Autowired
    private XssProperties xss;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // GET DELETE 不过滤
        HttpMethod method = request.getMethod();
        if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
            return chain.filter(exchange);
        }
        // 非json类型,不过滤
        if (!WebFluxUtils.isJsonRequest(exchange)) {
            return chain.filter(exchange);
        }
        // excludeUrls 不过滤
        String url = request.getURI().getPath();
        if (StringUtils.matches(url, xss.getExcludeUrls())) {
            return chain.filter(exchange);
        }
        ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
        return chain.filter(exchange.mutate().request(httpRequestDecorator).build());

    }

    private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange) {
        ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public Flux<DataBuffer> getBody() {
                Flux<DataBuffer> body = super.getBody();
                return body.buffer().map(dataBuffers -> {
                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                    DataBuffer join = dataBufferFactory.join(dataBuffers);
                    byte[] content = new byte[join.readableByteCount()];
                    join.read(content);
                    DataBufferUtils.release(join);
                    String bodyStr = new String(content, StandardCharsets.UTF_8);
                    // 防xss攻击过滤
                    bodyStr = HtmlUtil.cleanHtmlTag(bodyStr);
                    // 转成字节
                    byte[] bytes = bodyStr.getBytes();
                    NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
                    DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
                    buffer.write(bytes);
                    return buffer;
                });
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
                httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                return httpHeaders;
            }

        };
        return serverHttpRequestDecorator;
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

5.防sql注入

package com.zhdj.gateway.filter;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhdj.gateway.config.properties.SqlProperties;
import com.zhdj.gateway.config.properties.XssProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @Author {Yangb}
 * @Date: 2023/10/30/ 10:40
 * @description 防sql注入
 */
@Slf4j
@Component
@ConditionalOnProperty(value = "security.sql.enabled", havingValue = "true")
public class SqlInjectionFilter implements GlobalFilter, Ordered {

    @Autowired
    private SqlProperties sqlProperties;

    /**
     * SQL注入正则判断
     */
    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);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求路径、请求方法、请求参数
        ServerHttpRequest request = exchange.getRequest();
        String requestPath = request.getPath().toString();
        HttpMethod method = request.getMethod();
        MediaType contentType = request.getHeaders().getContentType();
        log.info("SqlInjectionFilter start requestPath:{}, method:{}, contentType:{}", requestPath, method, contentType);
        // 白名单放行
        String[] whiteListArray = sqlProperties.getExcludeUrls().toArray(new String[0]);
        for (String path : whiteListArray) {
            if (requestPath.startsWith(path)) {
                log.info("SqlInjectionFilter white list:" + requestPath);
                return chain.filter(exchange);
            }
        }
        // 判断Param是否存在SQL注入
        AtomicBoolean isSqlInjection = new AtomicBoolean(false);
        request.getQueryParams().forEach((key, values) -> {
            for (String value : values) {
                if (StringUtils.hasText(value) && checkSqlInjection(value)) {
                    isSqlInjection.set(true);
                    return;
                }
            }
        });
        if (isSqlInjection.get()) {
            return errorResponse(exchange.getResponse());
        }
        // contentType不为空,一般说明body中有参数,判断body中是否存在SQL注入,如果存在则直接返回错误信息
        if (contentType != null) {
            return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
                // 取出body中的参数
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                    DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                    DataBufferUtils.retain(buffer);
                    return Mono.just(buffer);
                });
                String bodyString = new String(bytes, StandardCharsets.UTF_8);
                if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
                    if (StringUtils.hasText(bodyString) && checkJsonBody(bodyString)) {
                        isSqlInjection.set(true);
                    }
                } else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
                    if (StringUtils.hasText(bodyString) && checkFormUrlencoded(bodyString)) {
                        isSqlInjection.set(true);
                    }
                } else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
                    if (StringUtils.hasText(bodyString) && checkFormData(bodyString)) {
                        isSqlInjection.set(true);
                    }
                }
                //  如果存在sql注入,直接拦截请求
                if (isSqlInjection.get()) {
                    log.error("SqlInjectionFilter {} - [{}] 参数:{}, 包含不允许sql的关键词,请求拒绝", method, requestPath, bodyString);
                    return errorResponse(exchange.getResponse());
                }
                // 重新包装ServerHttpRequest
                ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                    @Override
                    public Flux<DataBuffer> getBody() {
                        return cachedFlux;
                    }
                };
                // 用新的ServerHttpRequest改变交换对象
                ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
                // 使用默认的messageReaders读取正文字符串
                return ServerRequest.create(mutatedExchange, HandlerStrategies.withDefaults().messageReaders()).bodyToMono(String.class).doOnNext(objectValue -> {
                    log.info("SqlInjectionFilter end requestPath:{}, method:{}, contentType:{}", requestPath, method, contentType);
                }).then(chain.filter(mutatedExchange));
            });
        }
        log.info("SqlInjectionFilter end requestPath:{}, method:{}, contentType:{}", requestPath, method, null);
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 2;
    }
    /**
     * 返回错误响应
     *
     * @param response 响应
     * @return Mono<Void>
     */
    private Mono<Void> errorResponse(ServerHttpResponse response) {
        response.setStatusCode(HttpStatus.BAD_REQUEST);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", HttpStatus.BAD_REQUEST.value());
        jsonObject.put("msg", "invalid request params");
        String body = jsonObject.toJSONString();
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Flux.just(buffer));
    }

    /**
     * 判断输入的字符串是否包含SQL注入
     *
     * @param str 输入的字符串
     * @return 如果输入的字符串包含SQL注入,返回 true,否则返回 false。
     */
    private boolean checkSqlInjection(String str) {
        str = str.toLowerCase();
        Matcher matcher = sqlPattern.matcher(str);
        if (matcher.find()) {
            log.error("SqlInjectionFilter 参数[{}]中包含不允许sql的关键词", str);
            return true;
        }
        return false;
    }

    /**
     * 检测application/x-www-form-urlencoded是否包含SQL注入关键字
     *
     * @param bodyString 请求体
     * @return 是否包含SQL注入关键字
     */
    private boolean checkFormUrlencoded(String bodyString) {
        String[] params = bodyString.split("&");
        for (String param : params) {
            String[] keyValue = param.split("=");
            if (keyValue.length == 2) {
                // 判断是否为空
                if (StrUtil.isBlank(keyValue[1])) {
                    continue;
                }
                if (checkSqlInjection(keyValue[1])) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 检测application/json是否包含SQL注入关键字
     *
     * @param body 请求体
     * @return 是否包含SQL注入关键字
     */
    private boolean checkJsonBody(String body) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode rootNode = objectMapper.readTree(body);
            return checkJsonNode(rootNode);
        } catch (IOException e) {
            log.error("SqlInjectionFilter Error while parsing JSON body", e);
            return false;
        }
    }


    private boolean checkJsonNode(JsonNode node) {
        if (node.isValueNode()) {
            return checkSqlInjection(node.asText());
        } else if (node.isObject()) {
            Iterator<Map.Entry<String, JsonNode>> fieldsIterator = node.fields();
            while (fieldsIterator.hasNext()) {
                Map.Entry<String, JsonNode> field = fieldsIterator.next();
                // 判断是否为空
                if (field.getValue().isNull()) {
                    continue;
                }
                if (checkJsonNode(field.getValue())) {
                    return true;
                }
            }
        } else if (node.isArray()) {
            for (JsonNode childNode : node) {
                if (checkJsonNode(childNode)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 检测multipart/form-data是否包含SQL注入关键字
     *
     * @param bodyString 请求体
     * @return 是否包含SQL注入关键字
     */
    public boolean checkFormData(String bodyString) {
        bodyString = bodyString.replaceAll("\r", "");
        String[] parts = bodyString.split("\n");
        for (int i = 0; i < parts.length; i++) {
            String part = parts[i];

            if (part.contains("Content-Disposition: form-data;")) {
                // 文件类型不检测
                if (part.contains("file")) {
                    log.info("SqlInjectionFilter file skip");
                    return false;
                }
                String value = parts[i + 2];
                if (StrUtil.isBlank(value)) {
                    continue;
                }
                if (checkSqlInjection(value)) {
                    return true;
                }
            }
        }
        return false;
    }


}

6.yml 配置

在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值