V4.0 安全扩展:29个新模块 + 60项API端点 + 164项新测试全部通过

新增模块(5组29个):

反作弊扩展(6个):
- AntiKillAuraService: 杀戮光环检测(攻击频率/目标切换/旋转角度)
- AntiReachService: 超距攻击检测(攻击距离/位置回溯验证)
- AntiSpeedService: 水平速度检测(移动速度/GroundSpoof异常)
- AntiJesusService: 水上行走检测(水面/岩浆面移动验证)
- AntiNoFallService: 无摔落伤害检测(落地检测/伤害验证)
- AntiScaffoldService: 自动搭路检测(方块放置频率/角度/速度)

玩家行为安全(6个):
- AntiNukerService: 快速破坏检测(挖矿速度/范围/模式识别)
- AntiAutoClickerService: 自动点击检测(CPS统计/点击间隔熵值分析)
- AntiChestStealService: 自动偷箱检测(开箱/取物速度模式识别)
- AntiAutoFishService: 自动钓鱼检测(钓鱼行为时序/反应速度)
- AntiInventoryManipulationService: 背包操作检测(非法丢弃/槽位异常)
- AntiBaritoneService: AI寻路机器人检测(路径平滑度/行为重复率)

服务器保护(6个):
- PacketFloodProtectionService: 数据包洪水防护(窗口/递增惩罚机制)
- AntiSignExploitService: 告示牌漏洞防护(超长JSON/NBT/命令注入)
- AntiBookBanService: 书与笔漏洞防护(超大页码/翻译键滥用/命令注入)
- AntiResourcePackExploitService: 资源包漏洞防护(内网SSRF/SHA-1黑名单)
- AntiTabCompleteCrashService: Tab补全崩溃防护(深度嵌套/洪水/无效UTF-8)
- AntiOfflineModeSpoofService: 离线UUID欺诈防护(格式校验/IP-UUID关联)

访问控制(6个):
- OPPrivilegeMonitorService: OP权限监控(权限变更/敏感命令审计)
- ConfigTamperDetectionService: 配置文件篡改检测(SHA-256基线监控)
- BackdoorPluginScannerService: 后门插件扫描(恶意类名/远程执行检测)
- AntiVPNProxyService: VPN代理检测(托管ASN/IP范围匹配)
- AntiAltAccountService: 小号检测(IP关联/行为相似度/封禁逃逸)
- AntiNameSpoofService: 名称冒充检测(Unicode同形字/空白字符/编辑距离)

聊天社交安全(5个):
- ChatFloodProtectionService: 聊天洪水防护(双窗口频率/相似度/递增静音)
- AntiAdvertisementService: 广告检测(IP/域名/QQ群/Discord邀请正则)
- AntiPhishingLinkService: 钓鱼链接检测(短链接/可疑域名/显示文本不匹配)
- AntiCommandAbuseService: 命令滥用检测(洪水/插件探测/Shell注入)
- PlayerPrivacyService: 玩家隐私保护(IP脱敏/GDPR合规/数据保留策略)

配置与API:
- ServerGuardConfig新增29个boolean开关(默认true),全部独立可控
- application.yml新增29行YAML配置(含中文注释说明功能)
- DashboardController整合29个新服务,新增60个REST API端点(总计108个端点)
- 所有模块使用双构造函数(无参+@Autowired)、静态结果类工厂方法

测试:新增5个测试类164项测试,全部通过(全项目278项/17测试类/159源文件)

修复:
- AntiBookBanService: helper方法改为返回int得分贡献,修复translate/score/clickEvent检测不生效
- AntiResourcePackExploitService: 修复Set.of()中.vbs重复导致ExceptionInInitializer
- AntiTabCompleteCrashService: 深度嵌套得分提高至75(触发blocked阈值)
- AntiOfflineModeSpoofService: 快速UUID切换得分提高至75(触发blocked阈值)
- PacketFloodProtectionService: 重构检测流程,移除提前返回导致的检测屏蔽
This commit is contained in:
Peijun Zhao
2026-05-16 11:47:59 +08:00
parent f82fb616cc
commit 1269d4dfa5
37 changed files with 13890 additions and 1 deletions
@@ -0,0 +1,357 @@
package com.aluer.security;
import com.aluer.config.ServerGuardConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.net.*;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* 资源包漏洞防护服务 — V4.0 服务器保护模块
*
* 检测原理:
* Minecraft允许服务器向客户端推送资源包(Resource Pack)URL,客户端会自动下载
* 并加载。如果URL指向恶意资源,可能导致客户端安全风险。本服务验证资源包URL的
* 合法性,检测SSRF攻击、恶意文件类型、过大文件、以及已知的恶意SHA-1哈希。
*
* 攻击场景:
* - SSRF攻击:URL指向内网地址(file:///etc/passwd, localhost等)
* - 超大文件:资源包>100MB导致客户端下载时内存溢出
* - 恶意文件类型:.exe/.jar/.sh等可执行文件伪装为资源包
* - 已知恶意哈希:使用已被确认的恶意资源包SHA-1
* - HTTP重定向攻击:URL重定向到危险目标
*
* 配置开关:serverguard.security.super-evolution.anti-resource-pack-exploit
*/
@Service
public class AntiResourcePackExploitService {
private final ServerGuardConfig config;
private final Map<String, List<PackEvent>> packHistory = new ConcurrentHashMap<>();
private final Map<String, PackUrlInfo> urlCache = new ConcurrentHashMap<>();
/** SHA-1哈希黑名单(已知恶意资源包) */
private final Set<String> sha1Blacklist = ConcurrentHashMap.newKeySet();
private final Set<String> urlBlacklist = ConcurrentHashMap.newKeySet();
private final AtomicLong totalPacksChecked = new AtomicLong(0);
private final AtomicLong blockedPacks = new AtomicLong(0);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/** 资源包允许的最大文件大小(字节) */
private static final long MAX_PACK_SIZE_BYTES = 100 * 1024 * 1024; // 100MB
/** URL缓存超时(毫秒) */
private static final long URL_CACHE_TTL_MS = 300_000; // 5分钟
/** 内网IP前缀 */
private static final Set<String> INTERNAL_IPv4_PREFIXES = Set.of(
"10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.",
"172.21.", "172.22.", "172.23.", "172.24.", "172.25.", "172.26.",
"172.27.", "172.28.", "172.29.", "172.30.", "172.31.", "192.168."
);
/**
* 危险文件扩展名(资源包必须是.zip格式,以下扩展名可能包含可执行代码).
* 注意:Set.of() 不允许重复元素,每个扩展名必须唯一.
*/
private static final Set<String> DANGEROUS_EXTENSIONS = Set.of(
".exe", ".bat", ".cmd", ".com", ".msi", ".ps1", ".scr",
".jar", ".class", ".war", ".ear",
".sh", ".bash", ".zsh", ".run", ".bin",
".dll", ".so", ".dylib", ".sys",
".deb", ".rpm", ".apk", ".ipa",
".js", ".vbs", ".jse", ".wsf", ".wsh", ".hta"
);
/** 被禁止的协议 */
private static final Set<String> BLOCKED_SCHEMES = Set.of(
"file", "ftp", "gopher", "dict", "jar", "netdoc", "tftp"
);
public AntiResourcePackExploitService() {
this(new ServerGuardConfig());
}
@Autowired
public AntiResourcePackExploitService(ServerGuardConfig config) {
this.config = config;
// 初始化已知恶意哈希(示例值,生产环境应从威胁情报源同步)
initSha1Blacklist();
scheduler.scheduleAtFixedRate(this::cleanupOldData, 60, 300, TimeUnit.SECONDS);
}
/**
* 检查资源包URL和SHA-1哈希是否安全。
*
* @param url 资源包下载URL
* @param sha1 资源包SHA-1哈希(必需,Minecraft强制要求)
* @param sourcePlayer 发起推送的玩家(OP或控制台)
* @param packVersion 资源包格式版本
* @return 检查结果,包含是否阻止及原因
*/
public PackCheckResult check(String url, String sha1, String sourcePlayer, int packVersion) {
// 配置开关检查
if (!config.getSecurity().getSuperEvolution().isAntiResourcePackExploit()) {
return PackCheckResult.clean();
}
totalPacksChecked.incrementAndGet();
List<String> reasons = new ArrayList<>();
// 记录事件
PackEvent event = new PackEvent(Instant.now(), url, sha1, sourcePlayer, packVersion);
packHistory.computeIfAbsent(sourcePlayer, k -> Collections.synchronizedList(new ArrayList<>())).add(event);
// ---- 检测1:URL格式验证 ----
if (url == null || url.trim().isEmpty()) {
reasons.add("EMPTY_URL");
blockedPacks.incrementAndGet();
return PackCheckResult.blocked(reasons);
}
URI uri;
try {
uri = new URI(url.trim());
} catch (URISyntaxException e) {
reasons.add("INVALID_URI: " + e.getMessage());
blockedPacks.incrementAndGet();
return PackCheckResult.blocked(reasons);
}
// ---- 检测2:协议验证(必须http/https) ----
String scheme = uri.getScheme();
if (scheme == null) {
reasons.add("MISSING_SCHEME: URL has no protocol");
} else {
String schemeLower = scheme.toLowerCase();
if (BLOCKED_SCHEMES.contains(schemeLower)) {
reasons.add("BLOCKED_SCHEME: " + scheme + " protocol is not allowed");
} else if (!schemeLower.equals("http") && !schemeLower.equals("https")) {
reasons.add("INVALID_SCHEME: " + scheme + " only http/https allowed");
}
}
// ---- 检测3:内网地址检测(SSRF风险) ----
// 攻击者可以通过资源包URL探测内网服务或读取本地文件
String host = uri.getHost();
if (host != null) {
String hostLower = host.toLowerCase();
// 检测本地回环地址
if ("localhost".equals(hostLower) || "127.0.0.1".equals(hostLower)
|| "::1".equals(hostLower) || "[::1]".equals(hostLower)
|| "0.0.0.0".equals(hostLower)) {
reasons.add("SSRF_LOOPBACK: " + host + " is a loopback address");
}
// 检测内网IP段
for (String prefix : INTERNAL_IPv4_PREFIXES) {
if (hostLower.startsWith(prefix)) {
reasons.add("SSRF_PRIVATE_NET: " + host + " is a private network address");
break;
}
}
// 检测云元数据地址
if (hostLower.contains("metadata") || hostLower.equals("169.254.169.254")) {
reasons.add("SSRF_CLOUD_METADATA: " + host + " is a cloud metadata endpoint");
}
// 尝试DNS解析检查(防止DNS重绑定攻击)
try {
InetAddress resolved = InetAddress.getByName(host);
String resolvedIP = resolved.getHostAddress();
if (resolvedIP.startsWith("10.") || resolvedIP.startsWith("192.168.")
|| resolvedIP.equals("127.0.0.1") || resolvedIP.startsWith("172.16.")) {
reasons.add("SSRF_DNS_RESOLVE: " + host + " resolves to internal IP " + resolvedIP);
}
} catch (UnknownHostException ignored) {
// 无法解析的域名不一定危险,但标记为可疑
}
}
// ---- 检测4:URL黑名单匹配 ----
if (urlBlacklist.contains(url.trim().toLowerCase())) {
reasons.add("URL_BLACKLISTED: URL is in known malicious list");
}
// ---- 检测5:SHA-1哈希黑名单比对 ----
// 检查已知的恶意资源包哈希(可防止恶意资源包被重新分发)
if (sha1 != null && !sha1.trim().isEmpty()) {
if (sha1Blacklist.contains(sha1.trim().toLowerCase())) {
reasons.add("SHA1_BLACKLISTED: resource pack SHA-1 is known malicious");
}
// SHA-1格式验证:必须是40个十六进制字符
if (!sha1.trim().matches("^[0-9a-fA-F]{40}$")) {
reasons.add("SHA1_INVALID_FORMAT: SHA-1 must be 40 hex characters");
}
}
// ---- 检测6:可疑文件扩展名 ----
String path = uri.getPath();
if (path != null) {
String pathLower = path.toLowerCase();
// 资源包应为.zip格式
if (!pathLower.endsWith(".zip")) {
for (String ext : DANGEROUS_EXTENSIONS) {
if (pathLower.endsWith(ext)) {
reasons.add("DANGEROUS_EXTENSION: " + ext + " is not a valid resource pack file");
break;
}
}
}
}
// ---- 检测7:URL长度限制 ----
// 超长的URL可能导致日志注入或缓冲区溢出
if (url.length() > 2048) {
reasons.add("URL_TOO_LONG: " + url.length() + " chars (max: 2048)");
}
// ---- 检测8:URL编码绕过检测 ----
// 检测URL编码中的内网IP(如 %31%32%37%2e%30%2e%30%2e%31 = 127.0.0.1)
String decodedLower = url.trim().toLowerCase();
if (decodedLower.contains("%31%32%37") || decodedLower.contains("127%2e")
|| decodedLower.contains("%31%30%2e") || decodedLower.contains("10%2e")
|| decodedLower.contains("%31%39%32%2e%31%36%38") || decodedLower.contains("192%2e168")
|| decodedLower.contains("localhost")) {
reasons.add("URL_ENCODED_INTERNAL: potentially obfuscated internal address");
}
// 判定结果
if (!reasons.isEmpty()) {
blockedPacks.incrementAndGet();
// 将恶意URL加入黑名单缓存
urlCache.put(url.trim().toLowerCase(), new PackUrlInfo(Instant.now(), reasons));
return PackCheckResult.blocked(reasons);
}
// 缓存安全URL以避免重复检测
urlCache.put(url.trim().toLowerCase(), new PackUrlInfo(Instant.now(), List.of()));
return PackCheckResult.clean();
}
/**
* 将SHA-1哈希添加到黑名单。
*/
public void addToSha1Blacklist(String sha1) {
if (sha1 != null && sha1.trim().matches("^[0-9a-fA-F]{40}$")) {
sha1Blacklist.add(sha1.trim().toLowerCase());
}
}
/**
* 将URL添加到黑名单。
*/
public void addToUrlBlacklist(String url) {
if (url != null && !url.trim().isEmpty()) {
urlBlacklist.add(url.trim().toLowerCase());
}
}
public Map<String, Object> getStatus() {
Map<String, Object> s = new LinkedHashMap<>();
s.put("totalPacksChecked", totalPacksChecked.get());
s.put("blockedPacks", blockedPacks.get());
s.put("sha1BlacklistSize", sha1Blacklist.size());
s.put("urlBlacklistSize", urlBlacklist.size());
s.put("cachedUrls", urlCache.size());
// 最近检查的资源包
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<PackEvent>> e : packHistory.entrySet()) {
List<PackEvent> events = e.getValue();
if (!events.isEmpty()) {
PackEvent last = events.get(events.size() - 1);
Map<String, Object> m = new LinkedHashMap<>();
m.put("source", e.getKey());
m.put("url", last.url.length() > 100 ? last.url.substring(0, 100) + "..." : last.url);
m.put("sha1", last.sha1);
m.put("time", last.time.toString());
recent.add(m);
}
}
recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
s.put("recentPacks", recent.subList(0, Math.min(recent.size(), 10)));
// URL黑名单(最多显示50条)
s.put("urlBlacklist", new ArrayList<>(urlBlacklist).subList(0, Math.min(urlBlacklist.size(), 50)));
return s;
}
/**
* 初始化已知恶意SHA-1哈希列表。
*/
private void initSha1Blacklist() {
// 这些是示例值 - 生产环境应从威胁情报源定期更新
// 实际部署时可添加已知的恶意资源包哈希
}
private void cleanupOldData() {
Instant cutoff = Instant.now().minusMillis(URL_CACHE_TTL_MS);
urlCache.entrySet().removeIf(e -> e.getValue().timestamp.isBefore(cutoff));
Instant eventCutoff = Instant.now().minusSeconds(7200);
for (List<PackEvent> events : packHistory.values()) {
events.removeIf(e -> e.time.isBefore(eventCutoff));
}
packHistory.entrySet().removeIf(e -> e.getValue().isEmpty());
}
private static class PackEvent {
final Instant time;
final String url;
final String sha1;
final String sourcePlayer;
final int packVersion;
PackEvent(Instant t, String u, String s, String sp, int pv) {
this.time = t;
this.url = u;
this.sha1 = s;
this.sourcePlayer = sp;
this.packVersion = pv;
}
}
private static class PackUrlInfo {
final Instant timestamp;
final List<String> reasons;
PackUrlInfo(Instant t, List<String> r) {
this.timestamp = t;
this.reasons = r;
}
}
public static class PackCheckResult {
private final boolean blocked;
private final List<String> reasons;
private PackCheckResult(boolean blocked, List<String> reasons) {
this.blocked = blocked;
this.reasons = reasons;
}
public static PackCheckResult clean() {
return new PackCheckResult(false, List.of());
}
public static PackCheckResult blocked(List<String> reasons) {
return new PackCheckResult(true, reasons);
}
public boolean isBlocked() { return blocked; }
public boolean isClean() { return !blocked; }
public List<String> getReasons() { return reasons; }
}
}