本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.7节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。
指南7:防止代码注入
当有不可信的输入注入动态构造的代码中时,会引起代码注入攻击。一个明显的潜在漏洞是在Java代码中使用JavaScript代码。javax.script包定义了Java脚本引擎的接口和类,以及在Java代码中使用这些接口和类的框架。javax.script API的滥用,会导致攻击者在目标系统上执行任意代码。
这条指南是《The CERT® Oracle® Secure Coding Standard for Java™》[Long 2012]的“IDS00-J. Sanitize untrusted data passed across a trust boundary”的一个实例。
违规代码示例
下面的违规代码示例将不可信的用户输入嵌入了负责打印输入的JavaScript语句中。
private static void evalScript(String firstName)
throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");
engine.eval("print('"+ firstName + "')");
}```
攻击者可以传入一个别有用心的输入值,用以注入恶意的JavaScript代码。下面的这个示例展示了一个恶意的字符串,该字符串包含的JavaScript代码可以在目标系统上创建或覆盖现有的文件。
dummy');
var bw = new JavaImporter(java.io.BufferedWriter);
var fw = new JavaImporter(java.io.FileWriter);
with(fw) with(bw) {
bwr = new BufferedWriter(new FileWriter("config.cfg"));
bwr.write("some text"); bwr.close();
}
// ;`
这个示例中的脚本首先打印“dummy”,然后将“some text”写入一个名为config.cfg的配置文件中。这是一个可以导致任意代码执行的真实漏洞。
合规解决方案(白名单)
最好的防御代码注入漏洞的方式就是阻止包含可执行代码的用户输入。任何用于动态代码的用户输入,都必须进行无害化处理,例如,确保用户输入只包含白名单里的有效字符。最好是在数据输入后,通过使用用于存储和处理数据的提取方法,立即执行数据无害化处理。更多细节请参考“IDS00-J. Sanitize untrusted data passed across a trust boundary”[Long 2012]。如果用户名中必须包含某些特殊字符,那么必须先对它们进行标准化,然后再对它们进行表单输入验证处理。下面的合规解决方案使用了白名单来防止脚本引擎对未经处理的输入进行解析。
private static void evalScript(String firstName)
throws ScriptException {
// Allow only alphanumeric and underscore chars in firstName
// (modify if firstName may also include special characters)
if (!firstName.matches("[\\w]*")) {
// String does not match whitelisted characters
throw new IllegalArgumentException();
}
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");
engine.eval("print('"+ firstName + "')");
}```
####合规解决方案(安全沙盒)
另一种方式是使用安全管理器创建一个安全的沙盒(参见指南20)。应用程序应该阻止执行任意命令的脚本,如查询本地文件系统。双参数版本的doPrivileged()方法可用于处理更低特权的操作,这种情况下,应用程序本身必须执行更高特权的操作,但脚本引擎决不能拥有这样的特权。对于默认策略文件中那些新创建的保护域,RestrictedAccessControlContext类减少了授予它们的权限。这些有效权限是新创建的保护域和系统安全策略二者权限的交集。有关doPrivileged()方法的更多细节,请参考指南16。
下面的合规解决方案演示了如何在双参数版本的doPrivileged()中使用AccessControlContext。
class ACC {
private static class RestrictedAccessControlContext {
private static final AccessControlContext INSTANCE;
static {
INSTANCE =
new AccessControlContext(
new ProtectionDomain[] {
new ProtectionDomain(null, null) // No permissions
});
}
}
private static void evalScript(final String firstName)
throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
final ScriptEngine engine =
manager.getEngineByName("javascript");
// Restrict permission using the two-argument
// form of doPrivileged()
try {
AccessController.doPrivileged(
new PrivilegedExceptionAction
public Object run() throws ScriptException {
engine.eval("print('" + firstName + "')");
return null;
}
},
// From nested class
RestrictedAccessControlContext.INSTANCE);
} catch (PrivilegedActionException pae) {
// Handle error
}
}
}`
将这个方法和白名单结合在一起使用,可以获得更高的安全性。
适用性
未能防止代码注入可能导致任意代码的执行。