feat(v1.21-beta): 代码安全审计与全面优化
基线: v1.20 (e58137c) | 分支: agent1.21bate
## P0 严重修复 (14项)
- 安全: 命令注入漏洞修复 (SpecificTimeTaskService)
- 安全: 路径穿越漏洞修复 (AgentEndpoint)
- 安全: 下载安全加固 (AdvancedAsyncDownloader)
- 数据: CPU avg1/avg5/avg15 赋值错误修复 (CPUServiceImpl)
- 数据: 磁盘读写字节/速率赋值反转修复 (DiskServiceImpl)
- 逻辑: updateTaskInterval 丢失原始任务修复 (DynamicTaskService)
- 并发: GlobalConfig 100+ 静态字段 volatile + 并发集合
- 性能: SessionManager O(n)->O(1) Channel反向映射
- 并发: SessionManager 序列号 AtomicInteger
- 并发: CompensatedTrigger CAS 竞态修复
- 泄漏: UDPListenHandler ByteBuf 内存泄漏修复
- NPE: BaseNettyServer stop() 空指针修复
- 线程: AsyncCommandExecutor 无界线程池改有界+守护线程
- 泄漏: AdvancedAsyncDownloader 连接和线程池资源释放
## P1 重要修复 (5项)
- NetServiceImpl: Process 资源泄漏 + 超时控制
- DockerServiceImpl: 数组越界保护 + 资源释放
- MemoryServiceImpl: 除零风险修复
- SystemServiceImpl: 除零风险修复 + 异常处理规范化
## P2 代码质量 (6项)
- 100+ System.out/err -> AssertLog 日志框架
- e.printStackTrace() -> 结构化日志
- InterruptedException 中断状态恢复
- BaseNettyConfig Boolean 包装类型改基本类型
- BaseException cause 构造函数修复
- 新增安全配置项 (命令白名单/HMAC/下载安全/握手认证)
## 新增安全模块
- security/AuthHandshakeHandler.java - Netty握手认证
- security/HmacSignVerifier.java - HMAC签名验证
- security/SecureCommandExecutor.java - 安全命令执行器
- security/SecureFileDownloader.java - 安全文件下载器
- security/SecurityProperties.java - 安全配置属性
- security/SystemCommandRunner.java - 系统命令运行器
- 对应单元测试 4个
版本更新: pom.xml 0.0.1-SNAPSHOT -> 1.21-beta
application.yml 1.0 -> 1.21
涉及文件: 34个 (21 modified + 13 new)
代码变更: +1085/-868 lines
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package com.tongran.agent.client.security;
|
||||
|
||||
import com.tongran.agent.client.utils.AssertLog;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* 系统命令执行器(底层)
|
||||
*
|
||||
* <p>仅被 {@link SecureCommandExecutor} 调用,不对外暴露。
|
||||
* 使用 {@link ProcessBuilder} 直接传参数数组,不经过 shell,杜绝命令注入。
|
||||
*
|
||||
* <p>设计为可注入的 Bean,方便单元测试 mock。
|
||||
*
|
||||
* @author Senior Developer
|
||||
*/
|
||||
@Component
|
||||
public class SystemCommandRunner {
|
||||
|
||||
/** 业务线程池:命令执行是 IO 密集型,固定大小足够 */
|
||||
private final ExecutorService executor = Executors.newFixedThreadPool(
|
||||
Math.max(2, Runtime.getRuntime().availableProcessors()),
|
||||
r -> {
|
||||
Thread t = new Thread(r, "agent-cmd-exec");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 异步执行脚本
|
||||
*
|
||||
* @param scriptPath 脚本绝对路径(已通过白名单校验)
|
||||
* @param args 参数列表(已与脚本路径分离,不经过 shell)
|
||||
* @param timeout 超时
|
||||
* @param unit 超时单位
|
||||
*/
|
||||
public CompletableFuture<SecureCommandExecutor.CommandResult> execute(
|
||||
Path scriptPath, List<String> args, long timeout, TimeUnit unit) {
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
Process process = null;
|
||||
try {
|
||||
// 1. 确保脚本有执行权限(Linux/Unix)
|
||||
ensureExecutable(scriptPath);
|
||||
|
||||
// 2. 构建命令:第一个元素是脚本路径,后续是参数
|
||||
// 关键:不使用 /bin/sh -c,参数直接传给 ProcessBuilder,不经过 shell 解析
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(scriptPath.toString());
|
||||
if (args != null) {
|
||||
command.addAll(args);
|
||||
}
|
||||
|
||||
AssertLog.info("[CMD-EXEC] exec={} args={}", scriptPath, args);
|
||||
|
||||
// 3. 启动进程
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.redirectErrorStream(false); // 分别读 stdout/stderr
|
||||
process = pb.start();
|
||||
|
||||
// 4. 异步读取输出(防止 buffer 满死锁)
|
||||
Process finalProcess = process;
|
||||
CompletableFuture<String> stdoutFuture = CompletableFuture.supplyAsync(
|
||||
() -> readStream(finalProcess.getInputStream()), executor);
|
||||
CompletableFuture<String> stderrFuture = CompletableFuture.supplyAsync(
|
||||
() -> readStream(finalProcess.getErrorStream()), executor);
|
||||
|
||||
// 5. 等待进程结束(带超时)
|
||||
boolean finished = process.waitFor(timeout, unit);
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
AssertLog.error("[CMD-EXEC] 超时强杀: {}", scriptPath);
|
||||
return SecureCommandExecutor.CommandResult.fail("执行超时");
|
||||
}
|
||||
|
||||
// 6. 读取输出(给 2 秒缓冲让读流线程收尾)
|
||||
String stdout = stdoutFuture.get(2, TimeUnit.SECONDS);
|
||||
String stderr = stderrFuture.get(2, TimeUnit.SECONDS);
|
||||
int exitCode = process.exitValue();
|
||||
|
||||
AssertLog.info("[CMD-EXEC] done exit={} script={}", exitCode, scriptPath);
|
||||
return SecureCommandExecutor.CommandResult.ok(exitCode, stdout, stderr);
|
||||
|
||||
} catch (TimeoutException te) {
|
||||
return SecureCommandExecutor.CommandResult.fail("读取输出超时");
|
||||
} catch (Exception e) {
|
||||
AssertLog.error("[CMD-EXEC] 执行异常: {}", e.getMessage());
|
||||
return SecureCommandExecutor.CommandResult.fail("执行异常: " + e.getMessage());
|
||||
} finally {
|
||||
if (process != null && process.isAlive()) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
|
||||
/** 确保脚本有执行权限(仅对 POSIX 系统生效) */
|
||||
private void ensureExecutable(Path path) {
|
||||
try {
|
||||
Set<String> fileStores = new HashSet<>();
|
||||
path.getFileSystem().getFileStores().forEach(fs -> fileStores.add(fs.type()));
|
||||
|
||||
// 简单判断:如果是 POSIX 文件系统,加执行权限
|
||||
if (Files.getFileAttributeView(path, java.nio.file.attribute.PosixFileAttributeView.class) != null) {
|
||||
Set<PosixFilePermission> perms = new HashSet<>(Files.getPosixFilePermissions(path));
|
||||
perms.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
perms.add(PosixFilePermission.GROUP_EXECUTE);
|
||||
Files.setPosixFilePermissions(path, perms);
|
||||
}
|
||||
// Windows 不需要执行权限,直接跳过
|
||||
} catch (Exception e) {
|
||||
AssertLog.warn("设置执行权限失败(非致命): {} - {}", path, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取输入流到字符串 */
|
||||
private String readStream(java.io.InputStream is) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append('\n');
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sb.append("[read-error] ").append(e.getMessage());
|
||||
}
|
||||
// 限制输出长度,防止日志爆炸
|
||||
String result = sb.toString();
|
||||
if (result.length() > 8192) {
|
||||
return result.substring(0, 8192) + "...[truncated]";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 优雅关闭 */
|
||||
public void shutdown() {
|
||||
executor.shutdown();
|
||||
try {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user