1. 项目概述:为什么说字符串拼接执行命令是定时炸弹?
十年前我刚入行时,也习惯用Runtime.getRuntime().exec("ping " + targetIP)这样的方式执行系统命令,直到有次线上环境因为用户输入包含特殊字符导致整个服务崩溃。这种在Java中通过字符串拼接构造命令行参数的做法,本质上是在用字符串处理二进制协议,就像用快递单贴满整个包裹表面来运送物品——看似方便,实则隐患重重。
现代操作系统执行命令的本质是进程间通信(IPC),命令行参数通过二进制协议传递。当我们用字符串拼接构造命令时,相当于把结构化数据强行压平成文本,必然会遇到以下典型问题:
- 参数中包含空格时会被错误分割(如文件名
my document.pdf变成两个参数) - 特殊字符(
|&<>等)会触发shell的意外解析 - 字符编码不一致导致二进制数据损坏
- 无权限校验可能执行危险命令
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心问题解析:字符串拼接的七宗罪
2.1 参数分割陷阱
假设我们需要用ImageMagick转换用户上传的图片:
java复制// 危险示例
String cmd = "convert " + uploadPath + " -resize 800x600 " + outputPath;
Runtime.getRuntime().exec(cmd);
当上传路径包含空格时(如/uploads/用户A的头像.jpg),实际执行的命令会变成:
code复制convert /uploads/用户A的头像.jpg -resize 800x600 /output/processed.jpg
此时shell会错误地将路径分割为/uploads/用户A的头像.jpg和-resize两个参数,导致执行失败。正确的做法是使用参数数组:
java复制Runtime.getRuntime().exec(new String[]{
"convert",
uploadPath, // 作为一个整体参数传递
"-resize",
"800x600",
outputPath
});
2.2 Shell注入漏洞
考虑一个服务器监控脚本:
java复制String userInput = request.getParameter("host");
Runtime.getRuntime().exec("ping -c 4 " + userInput);
当攻击者输入8.8.8.8; rm -rf /时,实际执行的命令是:
code复制ping -c 4 8.8.8.8; rm -rf /
这种漏洞在Web应用中尤其危险。防御措施包括:
- 使用参数数组而非字符串拼接
- 对输入进行白名单校验
- 使用
ProcessBuilder替代Runtime.exec
2.3 字符编码问题
当处理非ASCII路径时:
java复制String cmd = "ls " + "中文目录";
byte[] bytes = cmd.getBytes(); // 依赖平台默认编码
如果系统默认编码是ISO-8859-1而终端使用UTF-8,会导致路径解析失败。应显式指定编码:
java复制new ProcessBuilder("ls", "中文目录")
.directory(new File("."))
.start();
3. 专业解决方案:ProcessBuilder的正确打开方式
3.1 基础用法示例
java复制ProcessBuilder pb = new ProcessBuilder(
"ffmpeg",
"-i", inputFile.getAbsolutePath(),
"-c:v", "libx264",
"-crf", "23",
outputFile.getAbsolutePath()
);
pb.redirectErrorStream(true); // 合并错误输出
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.info("FFmpeg output: {}", line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("FFmpeg failed with code " + exitCode);
}
3.2 环境变量控制
java复制ProcessBuilder pb = new ProcessBuilder("python3", "script.py");
// 继承当前环境变量
Map<String, String> env = pb.environment();
env.put("PYTHONPATH", "/opt/custom_libs");
env.remove("DISPLAY"); // 确保无GUI
pb.directory(new File("/opt/scripts")); // 设置工作目录
3.3 输入输出重定向
java复制// 将进程输出重定向到文件
File logFile = new File("output.log");
pb.redirectOutput(logFile);
// 从文件读取输入
pb.redirectInput(new File("input.txt"));
// 错误输出合并到标准输出
pb.redirectErrorStream(true);
4. 高级技巧与实战经验
4.1 超时控制实现
java复制Process process = pb.start();
boolean finished = process.waitFor(30, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
throw new TimeoutException("Process timed out");
}
4.2 跨平台兼容处理
java复制String[] cmd;
if (SystemUtils.IS_OS_WINDOWS) {
cmd = new String[]{"cmd", "/c", "dir"};
} else {
cmd = new String[]{"ls", "-l"};
}
4.3 进程管道连接
java复制ProcessBuilder grepPb = new ProcessBuilder("grep", "error");
ProcessBuilder logPb = new ProcessBuilder("cat", "/var/log/syslog");
// 将cat进程的输出连接到grep的输入
Process logProcess = logPb.start();
grepPb.redirectInput(logProcess.getInputStream());
Process grepProcess = grepPb.start();
// 读取grep输出
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(grepProcess.getInputStream()))) {
// 处理输出
}
5. 常见问题排查手册
5.1 命令执行无反应
可能原因:
- 命令不在PATH中 → 使用绝对路径
- 文件权限不足 → 检查
chmod +x - 工作目录错误 → 设置
directory()
诊断方法:
java复制pb.redirectError(ProcessBuilder.Redirect.INHERIT);
5.2 中文乱码问题
解决方案:
java复制Process proces
