文章
身份和安全性
| |||
|
Steffo Weber
使用 Oracle API Gateway 作为 XML 防火墙来阻止 Oracle Identity Federation 接收格式错误的 SAML 请求。
2013 年 7 月
下载
Oracle API Gateway
Oracle Identity Federation
最新的研究论文,包括 Juraj Somorovsky 等人撰写的 On Breaking SAML:Be Whoever You Want to Be,表明某些 SAML(安全断言标记语言)实现易受 XML 攻击。尽管 SAML 是一个已被广泛接受的协议,但也因其复杂性而出名。这可能导致每个实际部署都必须处理实现中的缺陷。Somorovsky 的论文标题可能有一定误导性,因为论文并不是关于破坏 SAML 本身的,而是关于破坏某些 SAML 实现的(包括 Salesforce、IBM 和 Shibboleth 实现)。
本文旨在向您介绍如何将 Oracle API Gateway (OAG) 用作 XML 防火墙来阻止 Oracle Identity Federation 接收格式错误的 SAML 请求。OAG 内置了可以检查 SAML 断言(例如,SOAP 消息中接收的断言)的功能,但是没有现成集成一个接收 SAML 请求(其中也包括格式错误的 XML 消息)的 IdP。本文假设您有点儿熟悉 SAML、IdP 和 SP 概念。
Oracle 的企业部署指南建议将 Oracle HTTP Server(OHS,Apache HTTP Server 的改编版)作为反向代理放置在 OIF 前端。OIF 是一个在 JEE 应用服务器上运行的 Web 应用程序。虽然 OHS 会中断流量并阻止最终用户直接访问 OIF,但它不会检查流量中是否存在恶意内容。这时 OAG 就能发挥作用了。OAG 被设计成一个 XML 防火墙,用于保护基于 SOAP 和 REST 的 Web 服务。因此将其用作企业网络防火墙后的第一道防线。我们将 OAG 配置成一个反向代理来运行。OAG 监听某个 TCP 端口(比如,8080),仅接受 GET 请求(可在 OAG 中定义路径时进行此配置)。

以下是配置 OAG 来保护 OIF 的大致步骤。
通常情况下,通过一条 HTTP GET 语句传输 SAML 请求,其中请求以 URL 参数形式发送:http://idp.com/fed?SAMLRequest=<encoded-stuff>。通常对请求进行压缩(使用 zlib),然后进行 Base64 编码。示例 1 显示了一个 SAML 请求示例。
GET /fed/idp/samlv20?SAMLRequest=<base64string> HTTP/1.1 Host: oag.example.com:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:14.0) Gecko/20100101 Firefox/14.0.1 Accept: text/html,application/xhtml+xml,application/ xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.7,de;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Referer: http://localhost:8080/fedletsample/ Pragma: no-cache Cache-Control: no-cache
首先,提取 URL 参数的值并进行解码。使用一个简单的筛选器即可完成:HTTP Header Attribute。该筛选器还能检索 URL 参数中的属性。我们将从 SAML 请求提取值,然后复制到 saml.request(图 2)。

下一步是解码 saml.request 并准备将其输入 OAG XML 筛选器。大多数 XML 内容筛选器可以处理 content.body 实体。
我们将使用一个 Java 库对 saml.request 进行解码。仅编译附加的 Java 类,然后将 JAR (SAMLLibrary.jar) 放在 ext/lib 下。注意,位于 apigateway/groups/topologylinks/Group1/instance-1/ext/lib/ 和 ./oagpolicystudio/jre/lib/ext/ 下。重启 Policy Studio 和网关(不确定是否绝对必要,但是管用)。
我们将使用脚本筛选器对 saml.request 进行解码。选择 Groovy 语言(示例 2);要使用 OAG 内容筛选器,content.body 必须用 DOM 形式表示。此时,SAML 请求以一种可由 OAG 内容筛选器分析的形式存在。
import com.vordel.mime.XMLBody;
import com.vordel.mime.HeaderSet;
import com.vordel.mime.ContentType;
import com.vordel.mime.ContentType.Authority;
import java.lang.String;
import com.vordel.common.base64.Decoder;
import com.oracle.identity.SamlRequest;
import groovy.xml.DOMBuilder;
import groovy.xml.dom.DOMCategory;
def invoke(msg)
{
def saml = msg.get("saml.request");
def reader = new StringReader(SamlRequest.decode(saml));
def samlRequest = DOMBuilder.parse(reader);
def samlMimeBody = new XMLBody(new HeaderSet(),new
ContentType(ContentType.Authority.MIME,"text/xml"), samlRequest);
msg.put("content.body",samlMimeBody);
return true;
}
现在,我们将应用一个筛选器来检查 SAML 请求是否遵循 SAML 协议规范。首先,我们需要获取 XSD(下载位置:http://docs.oasis-open.org/security/saml/v2.0/saml-schema-protocol-2.0.xsd)。通过“XML Schemas > Add schema”将 XSD 添加到 OAG 模式缓存,如图 3 所示。

现在可将一个 Schema Validation 筛选器拖到画布上,并配置它来检查是否遵守 SAML 协议规范。在本例中,我们首先应用 Threatening Content 筛选器,它将运行一系列正则表达式来识别攻击签名(请参见:OAG 文档,2013 年)。我们还应用 XML-Complexity 筛选器。该筛选器的配置允许您指定 XML 文档的最大节点数、每个节点的子节点以及每个节点的属性。该筛选器还针对 DoS 攻击提供有价值的保护

内容检查完成后,可将请求传递给身份提供程序 (IdP),在本例中是 Oracle Identity Federation。
要将 OAG 配置成代理,OAG 必须能够接收某个端口上的请求并将该请求(检查内容之后)转发给实际服务。我们首先看看 OAG 如何接收消息。
默认情况下,OAG 启用自带的默认服务(通常监听:8080)。现在可以添加到服务的路径。(例如,正如 Apache mod_proxy ProxyPassReverse 指令所指定的,路径大致对应一个 URL 的一部分。)路径是客户端浏览器调用的 URL。在本例中,我们将路径定义为 /fed/idp。并且为路径分配一个 HTTP 方法,可通过该方法对其进行访问。在本例中,我们知道客户端使用 HTTP GET 方法传输 SAML 请求。我们将这个“GET”指定为方法。这意味着,如果客户端通过 POST 方法访问 <apigateway:8080/fed/idp>,网关将拒绝该请求(通常使用“Access Denied”消息)。如果相应地配置了端口和路径,您可以指定上述制定的策略。(使用图 5 所示的“Policies”选项卡)。
下一步是使用将请求转发到 IdP 的筛选器增强该策略(与内容筛选器相比,“筛选器”一词有些恼人,因为它们只是转换消息的模块,并不执行实际筛选)。我们将使用以下筛选器:
路径 /idp/fed 的完整策略如图 6 所示。

然而,要进行实际部署,还需要其他步骤:
一次性注销。上述策略假定每个 /fed/idp 访问都包含一个 SAML 请求。但可能并不总是这样。基于 SAML的注销也可以使用 /fed/idp。在本例中,必须增强上述策略来解释注销。这也非常容易实现。
使用 XML 防火墙(如 OAG)有一个很大的好处是,如果准备就绪的产品突然出现一个漏洞,可轻松做出反应。例如,当有人发现发送恶意消息可以威胁某个服务时,就是这种情况。如果攻击模式已知,您可以使用 OAG 的内容筛选器、编写操作(Groovy、Jython、JavaScript)甚至通过部署自己的 Java 类(就像我们使用解码器做的一样)来检查是否存在该模式。
成熟的攻击模式可能太过复杂,您无法快速编写一个 OAG 筛选器来保护 OIF 免受格式错误的 SAML 请求攻击。该解决方案在将请求传递给 OIF 之前先验证用户的身份。为了防止用户输入口令两次(一次是通过 OAG 将 SAML 请求发送到 OIF,一次是在 OIF 上进行身份验证),您可以使用一个 web-SSO 工具,如 Oracle Access Manager,该工具可以很好地与 OAG 集成。可以将 OIF 配置为使用 Access Manager 作为身份验证引擎(这是一个现成的配置)。/fed/idp 的这个 OAG 策略只需稍作修改。消息流如下:
注意,仅当 IdP 实现过程中出现攻击时(例如,通过 CERT 新闻),才需要这个备用设置。这是计划 B。将 OIF 重新配置为使用 Access Manager 作为身份验证引擎只需要几分钟。重新部署这个增强的策略,这将检查经过身份验证的用户,只需几分钟时间。
另一种方法是将带一个可选的 webgate 的反向代理 (OHS)(作为计划 B 的一部分启用)放到 OAG 前端(同一台服务器上),并在此终止 SSL 流量(图 7)。

这将允许您解释两件事:
这将需要(作为计划 B 的一部):
同样,这还意味着可在几分钟内对这类攻击做出反应。不是太糟糕,对不对?
package com.oracle.identity;
// Shamelessly copied from https://forums.oracle.com/thread/977401.
// Kudos go to an unknown coder.
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipException;
import org.apache.commons.codec.binary.Base64;
public class SamlRequest {
/**
* * @param args
*/
public static String decode(String encSAMLRequest){
String ret = null;
SamlRequest samlRequest = null; //the xml is compressed (deflate) and encoded (base64)
byte[] decodedBytes = null;
try {
decodedBytes = new Base64().decode(encSAMLRequest.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
//try DEFLATE (rfc 1951) -- according to SAML spec
ret = new String(inflate(decodedBytes, true));
//return new SamlRequest(new String(inflate(decodedBytes, true)));
} catch (Exception ze) {
//try zlib (rfc 1950) -- initial impl didn't realize java docs are wrong
try {
System.out.println(new String(inflate(decodedBytes, false)));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//return new SamlRequest(new String(inflate(decodedBytes, false)));
}
return ret;
}
private static byte[] inflate(byte[] bytes, boolean nowrap) throws Exception {
Inflater decompressor = null;
InflaterInputStream decompressorStream = null;
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
decompressor = new Inflater(nowrap);
decompressorStream = new InflaterInputStream(new ByteArrayInputStream(bytes),
decompressor);
byte[] buf = new byte[1024];
int count;
while ((count = decompressorStream.read(buf)) != -1) {
out.write(buf, 0, count);
}
return out.toByteArray();
} finally {
if (decompressor != null) {
decompressor.end();
}
try {
if (decompressorStream != null) {
decompressorStream.close();
}
} catch (IOException ioe) {
/*ignore*/
}
try {
if (out != null) {
out.close();
}
} catch (IOException ioe) {
/*ignore*/
}
}
}
}
Steffo Weber 是一名 Oracle 身份管理架构师。在加入 Oracle 之前,他在 Sun Microsystems 从事了 10 多年的高可扩展性和身份管理系统工作。