2.1 IDS00-J净化穿越受信边界的非受信数据
许多程序会接受来自未经验证的用户数据、网络连接,以及其他来自于非受信域的不可信数据,然后它们会将这些数据(经过修改或不经修改)穿过受信边界送到另一个受信域中。通常,这些数据是以字符串的形式出现的,并且它们会有既定的内部数据结构,这种数据结构必须能够被子系统所解析。同时,必须净化这些数据,因为子系统可能并不具备处理恶意输入信息的能力,这些未经净化数据输入很有可能包含注入攻击。
值得注意的是,程序必须对传递给命令行解释器或者解析器的所有字符串数据进行净化,这样才能保证解析器处理后或者解释器处理后的字符串是无害的。
许多命令行解释器和解析器提供了自己的数据净化和数据验证的方法。当存在这样的方法时,应当优先考虑它们,因为相对而言,自定义的净化方法会忽略一些特殊的情况,或者会忽略在解析器中所隐含着的那些复杂性。另一个问题是,当有新的功能添加到命令行解释器或者添加到解析器软件的时候,自定义的处理方法可能并不能得到很好的维护。
2.1.1 SQL注入
当初始的SQL查询被修改成另一个完全不同形式的查询的时候,就会出现SQL注入漏洞。执行这一被修改过的查询,可能会导致信息泄露或者数据被修改。防止SQL注入漏洞的主要方法是,净化并验证非受信输入,同时采用参数化查询的方法。
假设一个数据库具有用户名和密码数据,它可以用这些数据来对系统用户进行认证。每个用户名使用长度为8的字符串表示,密码使用长度为20的字符串表示。
一个用来验证用户的SQL命令如下所示:
SELECT * FROM db_user WHERE username='<USERNAME>' AND
????????????????????????????password='<PASSWORD>'
如果它可以返回任何记录,那么意味着用户名和密码是合法的。
然而,如果攻击者能够替代和中的任意字符串,它们可以使用下面的关于< USERNAME >的字符串进行SQL注入:
validuser' OR '1'='1
当将其注入到命令时,命令就会变成:
SELECT * FROM db_user WHERE username='validuser' OR '1'='1' AND
password=<PASSWORD>
如果validuser是一个有效的用户名,那么这条选择语句会选择出表中的validuser?记录。这个操作中不会使用密码,因为username='validuser'的判断条件为真;因此,不会检查那些在OR后面的条件。所以,只要在OR后面的部分是一个语法正确的SQL表达式,攻击者就可以由此获得validuser的访问权限。
同样,攻击者可以为提供字符串:
' OR '1'='1
这将会产生以下的命令:
SELECT * FROM db_user WHERE username='' AND password='' OR '1'='1'
这一次,‘1’=‘1’是永远成立的,它使得用户名和密码的验证是无效的,并且攻击者可以不需要正确的用户名或密码就能登录。
2.1.2 不符合规则的代码示例
在这个不符合规则的代码示例中,系统使用JDBC代码来认证用户。密码通过char数组传入,建立数据库连接,然后进行哈希编码。
遗憾的是,这段代码会出现SQL注入问题,因为在SQL语句中,sqlString?允许输入未经净化的输入参数。如前所述的攻击场景将再次出现。
class Login {
??public Connection getConnection() throws SQLException {
????DriverManager.registerDriver(new
??????????com.microsoft.sqlserver.jdbc.SQLServerDriver());
????String dbConnection =?
??????PropertyManager.getProperty("db.connection");
????// can hold some value like
????// "jdbc:microsoft:sqlserver://<HOST>:1433,<UID>,<PWD>"
????return DriverManager.getConnection(dbConnection);
??}
??String hashPassword(char[] password) {
????// create hash of password
??}
??public void doPrivilegedAction(String username, char[] password)
?????????????????????????????????throws SQLException {
????Connection connection = getConnection();
????if (connection == null) {
??????// handle error
????}
????try {
??????String pwd = hashPassword(password);
??????String sqlString = "SELECT * FROM db_user WHERE username = '"?
?????????????????????????+ username +
?????????????????????????"’ AND password = '" + pwd + "’";
??????Statement stmt = connection.createStatement();
??????ResultSet rs = stmt.executeQuery(sqlString);
??????if (!rs.next()) {
????????throw new SecurityException(
??????????"User name or password incorrect"
????????);
??????}
??????// Authenticated; proceed
????} finally {
??????try {
????????connection.close();
??????} catch (SQLException x) {
????????// forward to handler
??????}
????}
??}
}
2.1.3 符合规则的方案(PreparedStatement)
幸运的是,在JDBC类库中,提供了能够构建SQL命令并且处理非受信数据的API。在java.sql.PreparedStatement类中,可以对输入字符串进行转义,如果使用正确的话,可以防止SQL注入。下面的例子显示了基于组件的净化过程:
这个符合规则的方案修改了doPrivilegedAction()方法,使用PreparedStatement类来代替?java.sql.Statement。并且这段代码会验证username参数的长度,防止如果提交一个长用户名的时候可能会出现的攻击。
public void doPrivilegedAction(
??String username, char[] password
) throws SQLException {
??Connection connection = getConnection();
??if (connection == null) {
????// Handle error
??}
??try {
????String pwd = hashPassword(password);
????// Ensure that the length of user name is legitimate
????if ((username.length() > 8) {
??????// Handle error
????}
????String sqlString =?
??????"select * from db_user where username=? and password=?";
????PreparedStatement stmt = connection.prepareStatement(sqlString);
????stmt.setString(1, username);
????stmt.setString(2, pwd);
????ResultSet rs = stmt.executeQuery();
????if (!rs.next()) {
??????throw new SecurityException("User name or password incorrect");
????}
????// Authenticated, proceed
??} finally {
????try {
??????connection.close();
????} catch (SQLException x) {
??????// forward to handler
????}
??}
}
通过使用PreparedStatement?类的set*()方法,可以进行强类型检查。这样可以减少SQL注入漏洞,因为自动化例程会正确地转义双引号内的输入数据。需要注意的是,那些将数据插入数据库的查询也会使用PreparedStatement?。
XML注入
由于XML语言具有平台无关性、灵活性和相对简洁的特点,它适用于从远端过程调用到系统化存储、交换以及获取数据的各种场合。然而,因为XML的多功能性,所以XML也是被广泛攻击的对象。其中一种攻击称为XML注入。
如果用户有能力使用结构化XML文档作为输入,那么他能够通过在数据字段中插入XML标签来重写这个XML文档的内容。当XML解析器对这些标签进行解析和归类的时候,会将它们作为可执行的内容,从而导致重写许多数据成员。
下面是一段从一个在线商店应用中摘取出来的XML代码,主要用来查询后台数据库。用户可以指定该次购买的物品的数量。
<item>
??<description>Widget</description>
??<price>500.0</price>
??<quantity>1</quantity>
</item>
恶意用户可以在quantity?域中输入以下字符串来代替一个简单的数字:
1</quantity><price>1.0</price><quantity>1
结果会导致生成如下XML文档:
<item>
??<description>Widget</description>
??<price>500.0</price>
??<quantity>1</quantity><price>1.0</price><quantity>1</quantity>
</item>
通过用于XML的简单API(SAX)解析器(org.xml.sax?和javax.xml.parsers.SAXParser)可以解释该XML文件,这时第二个price域会覆盖第一个price域,从而使商品的价格被设置为$1。甚至于存在这样的可能,攻击者可以构造这样一个攻击:插入特殊字符,比如插入注释块或者CDATA分隔符,从而就可以扭曲XML文档正常表达的意思。
2.1.4 不符合规则的代码示例
在下面这段不符合规则的代码示例中,一个客户方法使用简单的字符串链接来创建一个XML查询,然后将其发送到服务器。在这时就有可能出现XML注入问题,因为这个方法并没有进行任何输入验证。
private void createXMLStream(BufferedOutputStream outStream,?
????????????????????????????String quantity) throws IOException {
??String xmlString;
??xmlString = "<item>\n<description>Widget</description>\n" +
??????????????"<price>500.0</price>\n" +
??????????????"<quantity>" + quantity + "</quantity></item>";
??outStream.write(xmlString.getBytes());
??outStream.flush();
}
2.1.5 符合规则的方案(白名单)
根据特定的数据和接收这些数据的命令解释器或者解析器的情况,必须使用一个适当的方法来净化这些非受信的用户输入。这个符合规则的方案使用白名单来净化输入数据。在这个方案中,处理方法要求输入的数字必须为0~9。
private void createXMLStream(BufferedOutputStream outStream,?
?????????????????????????????String quantity) throws IOException {
??// Write XML string if quantity contains numbers only.
??// Blacklisting of invalid characters can be performed?
??// in conjunction.
??if (!Pattern.matches("[0-9]+", quantity)) {
????// Format violation
??}
??String xmlString = "<item>\n<description>Widget</description>\n" +
?????????????????????"<price>500</price>\n" +
?????????????????????"<quantity>" + quantity + "</quantity></item>";
??outStream.write(xmlString.getBytes());
??outStream.flush();
}
2.1.6 符合规则的方案(XML模板)
一个更为通用的检查XML以防止注入的方法是,可以使用文档类型定义(Document Type Definition,DTD)或模板(schema)来进行验证。模板必须严格定义,从而防止通过注入的方式将一个合法的XML文档变成错误的文档。以下是一个用来验证XML文档片段的合适的模板:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="item">
??<xs:complexType>
????<xs:sequence>
??????<xs:element name="description" type="xs:string"/>
??????<xs:element name="price" type="xs:decimal"/>
??????<xs:element name="quantity" type="xs:integer"/>
????</xs:sequence>
??</xs:complexType>
</xs:element>
</xs:schema>
在schema.xsd文件中可以得到XML模板。在这个符合规则的方案中,可以采用这个模板来防止XML注入。它同时需要使用CustomResolver?类来防止XXE攻击。关于这个类和所谓的XXE攻击,可以参见如下代码示例中的描述。
private void createXMLStream(BufferedOutputStream outStream,
?????????????????????????????String quantity) throws IOException {
??String xmlString;
??xmlString = "<item>\n<description>Widget</description>\n" +
??????????????"<price>500.0</price>\n" +
??????????????"<quantity>" + quantity + "</quantity></item>";
??InputSource xmlStream = new InputSource(
????new StringReader(xmlString)
??);
??// Build a validating SAX parser using our schema
??SchemaFactory sf
????= SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
??DefaultHandler defHandler = new DefaultHandler() {
?????public void warning(SAXParseException s)
???????throws SAXParseException {throw s;}
?????public void error(SAXParseException s)
???????throws SAXParseException {throw s;}
?????public void fatalError(SAXParseException s)
???????throws SAXParseException {throw s;}
????};
??StreamSource ss = new StreamSource(new File("schema.xsd"));
??try {
????Schema schema = sf.newSchema(ss);
????SAXParserFactory spf = SAXParserFactory.newInstance();
????spf.setSchema(schema);
????SAXParser saxParser = spf.newSAXParser();
????// To set the custom entity resolver,
????// an XML reader needs to be created
????XMLReader reader = saxParser.getXMLReader();?
????reader.setEntityResolver(new CustomResolver());
????saxParser.parse(xmlStream, defHandler);
??} catch (ParserConfigurationException x) {
????throw new IOException("Unable to validate XML", x);
??} catch (SAXException x) {
????throw new IOException("Invalid quantity", x);
??}
??// Our XML is valid, proceed
??outStream.write(xmlString.getBytes());
??outStream.flush();
}
当XML可能已经载入还未处理的输入数据时,一般情况下使用XML模板或者DTD验证XML。如果还没有创建这样的XML字符串,那么在创建XML之前处理输入,这种方式性能
较高。
XML外部实体攻击(XXE)
一个XML文档可以从一个被称为实体的很小的逻辑块开始动态构建。实体可以是内部的、外部的或基于参数的。外部实体允许将外部文件中的XML数据包含进来。
根据XML W3C 4.4.3小节的建议?[W3C 2008]:“包含所需的验证”部分。
当一个XML处理器找到一个已经解析过的实体的引用的时候,为了验证这个文档,处理器必须包含它的替代字段。如果该实体是外部的,而且处理器不打算验证这个XML文档,那么处理器可以(但不必)包含该实体的替代字段。
攻击者通过操作实体的URI,使其指向特定的在当前文件系统中保存的文件,从而造成拒绝服务攻击或者程序崩溃,比如,指定/dev/random或者/dev/tty作为输入URI。这可能永久阻塞程序或者造成程序崩溃。这就称为XML外部实体攻击(XML external entity, XXE)。因为包含来作为外部实体的替代文本并不是必需的,并不是所有的XML解析器都存在这样的外部实体攻击的安全漏洞。
2.1.7 不符合规则的代码示例
下面这个不符合规则的代码示例尝试对evil.xml文件进行解析,并报告相关错误,然后退出。然而,SAX或者DOM(Document Object Model, 文档对象模型)解析器会尝试访问在SYSTEM属性中标识的URL,这意味着它将读取本地/dev/tty文件的内容。在POSIX系统中,读取这个文件会导致程序阻塞,直到可以通过计算机控制台得到输入数据为止。结果是,攻击者可以使用这样的恶意XML文件来导致系统挂起。
class XXE {
?private static void receiveXMLStream(InputStream inStream,
??????????????????????????????????????DefaultHandler defaultHandler)
????throws ParserConfigurationException, SAXException, IOException {
???SAXParserFactory factory = SAXParserFactory.newInstance();
???SAXParser saxParser = factory.newSAXParser();
???saxParser.parse(inStream, defaultHandler);
?}
?public static void main(String[] args)
????throws ParserConfigurationException, SAXException, IOException {
???receiveXMLStream(new FileInputStream("evil.xml"),
????????????????????new DefaultHandler());
?}
}
如果evil.xml文件中包含以下文本,程序会受到远程XXE攻击。
<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "file:/dev/tty">
<foo>bar</foo>
如果包含在异常里的信息是敏感的,则这个不符合规则的代码示例同样违反了规则ERR06-J。
2.1.8 符合规则的方案(EntityResolver)
这个符合规则的方案定义了一个CustomResolver类?,这个类实现了org.xml.sax.EntityResolver接口。它可以让SAX应用定制对外部实体的处理。setEntityResolver()方法可以将对应的SAX驱动实例注册进来。这个定制的处理器使用的是一个为外部实体定义的简单的白名单。当输入不能解析任何指定的、安全的实体源路径的时候,resolveEntity()方法会返回一个空的InputSource?对象。结果是,当解析恶意输入时,这个由自定义的解析器返回的空的InputSource对象会抛出java.net.MalformedURLException异常。需要注意的是,你必须创建一个XMLReader对象,以便通过这个对象来设置自定义的实体解析器。
下面是一个基于组件的净化示例。
class CustomResolver implements EntityResolver {
??public InputSource resolveEntity(String publicId, String systemId)
????throws SAXException, IOException {
????// check for known good entities
????String entityPath = "/home/username/java/xxe/file";
????if (systemId.equals(entityPath)) {
??????System.out.println("Resolving entity: " + publicId +
?????????????????????????" " + systemId);
??????return new InputSource(entityPath);
????} else {
??????return new InputSource(); // Disallow unknown entities
????????????????????????????????// by returning a blank path
????}
??}
}
class XXE {
??private static void receiveXMLStream(InputStream inStream,
???????????????????????????????????????DefaultHandler defaultHandler)
??????throws ParserConfigurationException, SAXException, IOException {
????SAXParserFactory factory = SAXParserFactory.newInstance();
????SAXParser saxParser = factory.newSAXParser();
????// To set the Entity Resolver, an XML reader needs to be created
????XMLReader reader = saxParser.getXMLReader();
????reader.setEntityResolver(new CustomResolver());
????reader.setErrorHandler(defaultHandler);
????InputSource is = new InputSource(inStream);
????reader.parse(is);
??}
??public static void main(String[] args)
??????throws ParserConfigurationException, SAXException, IOException {
????receiveXMLStream(new FileInputStream("evil.xml"),?
?????????????????????new DefaultHandler());
??}
}
2.1.9 风险评估
如果不在系统处理或存储用户输入之前净化数据,会导致注入攻击。
相关的漏洞 CVE-2008-2370描述了在Aapache Tomat 从4.1.0到4.1.37, 5.5.0 到 5.5.26以及 6.0.0 到6.0.16这些版本中的安全漏洞。当使用RequestDispatcher时,Tomcat会进行路径标准化,然后从URI中移除查询字串,这样会导致攻击者可以进行远程目录遍历攻击,并且在请求参数中通过..(两个点)来读取任意文件。