Files
AIagent/src/main/java/com/tongran/agent/client/security/SystemCommandRunner.java
T
lee 546eef3372 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
2026-07-22 12:58:52 +08:00

164 lines
6.5 KiB
Java

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();
}
}
}