diff --git a/node_modules/lodash/package.json b/node_modules/lodash/package.json new file mode 100644 index 00000000..68bc0065 --- /dev/null +++ b/node_modules/lodash/package.json @@ -0,0 +1,64 @@ +{ + "_from": "lodash@latest", + "_id": "lodash@4.17.21", + "_inBundle": false, + "_integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "_location": "/lodash", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "lodash@latest", + "name": "lodash", + "escapedName": "lodash", + "rawSpec": "latest", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "_shasum": "679591c564c3bffaae8454cf0b3df370c3d6911c", + "_spec": "lodash@latest", + "_where": "D:\\all\\JavaWeb\\devicesmgt", + "author": { + "name": "John-David Dalton", + "email": "john.david.dalton@gmail.com" + }, + "bugs": { + "url": "https://github.com/lodash/lodash/issues" + }, + "bundleDependencies": false, + "contributors": [ + { + "name": "John-David Dalton", + "email": "john.david.dalton@gmail.com" + }, + { + "name": "Mathias Bynens", + "email": "mathias@qiwi.be" + } + ], + "deprecated": false, + "description": "Lodash modular utilities.", + "homepage": "https://lodash.com/", + "icon": "https://lodash.com/icon.svg", + "keywords": [ + "modules", + "stdlib", + "util" + ], + "license": "MIT", + "main": "lodash.js", + "name": "lodash", + "repository": { + "type": "git", + "url": "git+https://github.com/lodash/lodash.git" + }, + "scripts": { + "test": "echo \"See https://travis-ci.org/lodash-archive/lodash-cli for testing details.\"" + }, + "version": "4.17.21" +} diff --git a/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/XsltInjectionGatewayFilter.java b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/XsltInjectionGatewayFilter.java new file mode 100644 index 00000000..e036b2b1 --- /dev/null +++ b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/XsltInjectionGatewayFilter.java @@ -0,0 +1,111 @@ +package com.bonus.sgzb.gateway.filter; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.core.Ordered; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Flux; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.MultiValueMap; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Pattern; + +/** + * 防御 XSLT 注入 Gateway 过滤器 + * 校验 Query / Form / JSON 中指定参数,白名单 + 关键字双重过滤 + */ +@Slf4j +@Component +public class XsltInjectionGatewayFilter implements GlobalFilter, Ordered { + + /* 仅允许字母、数字、_- */ + private static final Pattern WHITE = Pattern.compile("^[A-Za-z0-9_-]+$"); + + /* XSLT 危险关键字(大小写不敏感) */ + private static final Set BLACK_KEYS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + "xsl:", " CHECK_PARAMS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("type", "xsl", "xml", "stylesheet", "transform")) + ); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String path = request.getPath().value(); + + /* 1. 校验 Query 参数 */ + MultiValueMap queryParams = request.getQueryParams(); + for (String key : CHECK_PARAMS) { + List values = queryParams.get(key); + if (values != null) { + for (String v : values) { + if (!isValid(v)) { + log.warn("XSLT_INJECTION_QUERY ip={} uri={} param={} value={}", + request.getRemoteAddress(), path, key, v); + return reject(exchange); + } + } + } + } + + /* 2. 校验 Form 参数(application/x-www-form-urlencoded) */ + MediaType contentType = request.getHeaders().getContentType(); + if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) { + return exchange.getFormData() + .flatMap(form -> { + for (String key : CHECK_PARAMS) { + List values = form.get(key); + if (values != null) { + for (String v : values) { + if (!isValid(v)) { + log.warn("XSLT_INJECTION_FORM ip={} uri={} param={} value={}", + request.getRemoteAddress(), path, key, v); + return reject(exchange); + } + } + } + } + return chain.filter(exchange); + }); + } + + /* 3. JSON 参数留扩展(可继续读 body 校验) */ + return chain.filter(exchange); + } + + private boolean isValid(String value) { + if (value == null || value.isEmpty()) return true; + if (!WHITE.matcher(value).matches()) return false; + String lower = value.toLowerCase(Locale.ROOT); + return BLACK_KEYS.stream().noneMatch(lower::contains); + } + + private Mono reject(ServerWebExchange exchange) { + ServerHttpResponse resp = exchange.getResponse(); + resp.setStatusCode(HttpStatus.BAD_REQUEST); + byte[] body = "Illegal XSLT keyword/character in parameter".getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = resp.bufferFactory().wrap(body); + return resp.writeWith(Flux.just(buffer)); + } + + @Override + public int getOrder() { + /* 比 SecurityHeaderFilter 更早执行,先拦非法参数再加头 */ + return Ordered.HIGHEST_PRECEDENCE; + } +} \ No newline at end of file