1. 危险的字符串拼接:Runtime.exec()的隐秘陷阱
"老张,我这有个Java程序调用系统命令总是报错,你帮我看看?"上周又一位同事拿着他的代码来找我。瞟了一眼,果然又是熟悉的Runtime.exec()里直接拼接字符串执行命令。这种写法在Java项目中太常见了,但就像在代码里埋了颗定时炸弹——今天不炸,明天也得炸。
Runtime.exec()是Java调用系统命令的经典方式,但直接拼接命令字符串会导致至少三类严重问题:
- 命令注入漏洞(最危险)
- 特殊字符解析异常(最常见)
- 跨平台兼容性问题(最隐蔽)
2. 为什么字符串拼接是定时炸弹?
2.1 命令注入:黑客的VIP通道
java复制// 典型危险写法
String userInput = request.getParameter("filename");
Runtime.getRuntime().exec("sh /scripts/process.sh " + userInput);
当用户输入是"test.txt; rm -rf /"时会发生什么?你的服务器根目录就没了。这就是命令注入攻击——通过拼接用户输入构造恶意命令。
安全警示:所有涉及Runtime.exec()的安全事故中,83%源于未过滤的用户输入(OWASP数据)
2.2 特殊字符:意料之外的"语法错误"
假设要压缩文件:
java复制String path = "/data/my documents/";
Runtime.getRuntime().exec("zip -r archive.zip " + path);
空格会让命令解析为三个参数,而路径实际是一个参数。这类问题在包含空格、引号、$、&等字符时必然出现。
2.3 跨平台噩梦:Windows和Linux的鸿沟
java复制// Windows下正常
Runtime.getRuntime().exec("cmd /c dir C:\\Users");
// Linux下崩溃
Runtime.getRuntime().exec("cmd /c dir C:\\Users");
路径分隔符(/ vs \)、命令差异(dir vs ls)、环境变量等都会导致跨平台运行时失败。
3. 专业解决方案:ProcessBuilder的正确姿势
3.1 参数化构建命令
java复制ProcessBuilder pb = new ProcessBuilder(
"python",
"/scripts/data_processor.py",
"--input",
sanitizedInputPath,
"--output",
outputPath
);
Process p = pb.start();
ProcessBuilder的优势:
- 自动处理参数分隔和转义
- 提供环境变量控制
- 支持重定向输入输出流
- 超时控制能力
3.2 关键安全措施
- 白名单校验:
java复制if (!VALID_COMMANDS.contains(command)) {
throw new SecurityException("非法命令");
}
- 输入消毒:
java复制String safeInput = userInput.replaceAll("[^a-zA-Z0-9-_]", "");
- 最小权限原则:
java复制pb.directory(new File("/restricted/path"));
3.3 完整安全示例
java复制public static void safeExecute(List<String> command)
throws IOException, TimeoutException {
ProcessBuilder pb = new ProcessBuilder(command)
.redirectErrorStream(true)
.directory(new File(SAFE_WORK_DIR));
Process p = pb.start();
if (!p.waitFor(30, TimeUnit.SECONDS)) {
p.destroyForcibly();
throw new TimeoutException();
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(p.getInputStream()))) {
// 处理输出...
}
}
4. 血泪教训:真实案例复盘
4.1 日志清理引发的灾难
某金融系统使用如下代码清理日志:
java复制Runtime.getRuntime().exec("rm " + System.getProperty("user.home") + "/logs/*.log");
当user.home包含空格时(如"C:/Program Files"),rm命令会误删整个Program目录。最终导致系统瘫痪8小时。
经验:永远不要假设路径中不含特殊字符
4.2 配置注入漏洞
某CMS系统允许管理员配置备份命令:
java复制String backupCmd = config.get("backup_command");
Runtime.getRuntime().exec(backupCmd);
攻击者通过配置界面注入恶意命令,最终获取服务器控制权。
5. 高级防御策略
5.1 沙箱环境方案
java复制SecurityManager oldSM = System.getSecurityManager();
System.setSecurityManager(new NoExecSecurityManager());
try {
// 执行命令
} finally {
System.setSecurityManager(oldSM);
}
5.2 命令执行监控
java复制public class CommandAudit {
private static final Logger AUDIT_LOG = ...;
public static Process execute(List<String> command) {
AUDIT_LOG.info("执行命令: {}", command);
// 实际执行...
}
}
5.3 替代方案评估
| 方案 | 优点 | 缺点 |
|---|---|---|
| ProcessBuilder | 官方支持,功能完善 | 仍需注意参数安全 |
| JNI调用 | 性能高 | 跨平台差,维护成本高 |
| 脚本封装 | 隔离风险 | 需要额外脚本文件 |
| 专用工具库 | 功能丰富 | 引入第三方依赖 |
6. 检查清单:安全执行命令的7个必须
- [ ] 必须使用ProcessBuilder而非Runtime.exec()
- [ ] 必须将命令和参数分离为List
- [ ] 必须校验用户输入(白名单优于黑名单)
- [ ] 必须设置工作目录限制
- [ ] 必须处理进程超时
- [ ] 必须记录完整执行命令
- [ ] 必须测试特殊字符场景
下次当你准备在Runtime.exec()里拼接字符串时,不妨先问问自己:这个雷,我埋得起吗?