本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.9节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达)
指南9:防止LDAP注入
轻量级目录访问协议(Lightweight Directory Access Protocol,LDAP)允许应用程序执行远程操作,如在目录中搜索和修改记录。不足的输入处理(sanitization)和验证会导致LDAP注入攻击,并允许恶意用户通过使用目录服务来收集被限制的信息。
白名单可以用来限制用户输入,使其符合一个有效的字符列表。禁止加入白名单的字符或字符序列包括:Java命名和目录接口(Java Naming and Directory Interface,JNDI)的元字符和LDAP特殊字符,这些字符都列在了表1-1中。
注:*这是一个字符序列。
LDAP注入示例
试想一个LDAP数据交换格式(LDAP Data Interchange Format,LDIF)文件,其中包含的记录格式如下:
dn: dc=example,dc=com
objectclass: dcobject
objectClass: organization
o: Some Name
dc: example
dn: ou=People,dc=example,dc=com
ou: People
objectClass: dcobject
objectClass: organizationalUnit
dc: example
dn: cn=Manager,ou=People,dc=example,dc=com
cn: Manager
sn: John Watson
# Several objectClass definitions here (omitted)
userPassword: secret1
mail: john@holmesassociates.com
dn: cn=Senior Manager,ou=People,dc=example,dc=com
cn: Senior Manager
sn: Sherlock Holmes
# Several objectClass definitions here (omitted)
userPassword: secret2
mail: sherlock@holmesassociates.com
并且对有效用户名和密码的搜索经常使用下面这种形式:
(&(sn=<USERSN>)(userPassword=<USERPASSWORD>))
然而,攻击者可以通过在USERSN字段中输入S、在USERPASSWORD字段中输入来绕过身份验证。这样的输入将会使所有在USERSN字段中以S开头的记录被悉数查出。
如果验证例程存在LDAP注入风险,就会导致未经验证的用户登录。同样地,有类似问题的搜索例程就会将该目录下部分甚至全部数据暴露给攻击者。
违规代码示例
下面的违规代码示例使用LDAP协议来允许调用者通过searchRecord()方法来搜索目录中的记录。匹配到调用者提供的用户名和密码后,字符串过滤器将会从结果集中过滤出匹配的结果。
// String userSN = "S*"; // Invalid
// String userPassword = "*"; // Invalid
public class LDAPInjection {
private void searchRecord(String userSN, String userPassword)
throws NamingException {
Hashtable<String, String> env =
new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
try {
DirContext dctx = new InitialDirContext(env);
SearchControls sc = new SearchControls();
String[] attributeFilter = {"cn", "mail"};
sc.setReturningAttributes(attributeFilter);
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
String base = "dc=example,dc=com";
// The following resolves to (&(sn=S*)(userPassword=*))
String filter = "(&(sn=" + userSN + ")(userPassword=" +
userPassword + "))";
NamingEnumeration<?> results =
dctx.search(base, filter, sc);
while (results.hasMore()) {
SearchResult sr = (SearchResult) results.next();
Attributes attrs = (Attributes) sr.getAttributes();
Attribute attr = (Attribute) attrs.get("cn");
System.out.println(attr);
attr = (Attribute) attrs.get("mail");
System.out.println(attr);
}
dctx.close();
} catch (NamingException e) {
// Forward to handler
}
}
}
当恶意用户输入别有用心的值时,正如前面提到的,这个基本的身份验证方案没能对需要用户访问特权的信息查询作出限制。
合规解决方案
下面的合规解决方案使用白名单对用户输入进行无害化处理,使得filter字符串只包含有效字符。在这段代码中,userSN只可能包含字母和空格,而密码则只可能包含字母数字字符。
// String userSN = "Sherlock Holmes"; // Valid
// String userPassword = "secret2"; // Valid
// ... beginning of LDAPInjection.searchRecord() ...
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
String base = "dc=example,dc=com";
if (!userSN.matches("[\\w\\s]*") ||
!userPassword.matches("[\\w]*")) {
throw new IllegalArgumentException("Invalid input");
}
String filter = "(&(sn = " + userSN + ")(userPassword=" +
userPassword + "))";
// ... remainder of LDAPInjection.searchRecord() ...
当密码这样的数据库字段必须包含特殊字符时,至关重要的是,确保可信的数据以无害的形式存储在数据库中,并且在验证或比较用户输入前必须对其进行标准化。在缺乏广泛的标准化和白名单过滤的情况下,使用在JNDI和LDAP中有特殊含义字符的程序是极不安全的。特殊字符在被添加到白名单验证之前必须被转换为清洁的安全值。就像用户输入在被验证之前首先要被标准化一样。
适用性
未能对不可信输入做无害化处理可能导致信息泄露和特权升级。