为 OTN 撰稿
为 Oracle 技术网撰写技术文章,获得报酬的同时可提升技术技能。
了解更多信息
密切关注
OTN 架构师社区
OTN ArchBeat 博客 Facebook Twitter YouTube 随身播图标

阻止 IDP 接收格式错误的 SAML 请求

Steffo Weber

使用 Oracle API Gateway 作为 XML 防火墙来阻止 Oracle Identity Federation 接收格式错误的 SAML 请求。

2013 年 7 月

下载
download-icon13-1Oracle API Gateway
download-icon13-1Oracle 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 中定义路径时进行此配置)。

weber-oif-oag-fig01
图 1:部署架构概览

以下是配置 OAG 来保护 OIF 的大致步骤。

  • 从 HTTP 请求中提取 SAML 请求。
  • 将 SAML 请求转换成一个 XML 文本文档。
  • 使用 OAG 的即需即用筛选器分析 XML 对象。

定义 Oracle Indentity Federation 保护策略

提取 SAML 请求

通常情况下,通过一条 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
示例 1:一个简单的 SAML 请求

首先,提取 URL 参数的值并进行解码。使用一个简单的筛选器即可完成:HTTP Header Attribute。该筛选器还能检索 URL 参数中的属性。我们将从 SAML 请求提取值,然后复制到 saml.request(图 2)。

weber-oif-oag-fig02
图 2:提取 SAMLRequest URL 参数的值

解码 SAMLRequest 并准备 content.body

下一步是解码 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;
}
示例 2:生成 DOM 形式的 content.body

使用 Oracle API Gateway 筛选内容

现在,我们将应用一个筛选器来检查 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 所示。

weber-oif-oag-fig03
图 3:加载 SAML 模式。

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

weber-oif-oag-fig04
图 4:内容筛选器

内容检查完成后,可将请求传递给身份提供程序 (IdP),在本例中是 Oracle Identity Federation。

保护 Oracle Identity Federation

将 Oracle API Gateway 配置成代理

要将 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”选项卡)。
weber-oif-oag-fig05
图 5:在 OAG 中定义端口和路径。

下一步是使用将请求转发到 IdP 的筛选器增强该策略(与内容筛选器相比,“筛选器”一词有些恼人,因为它们只是转换消息的模块,并不执行实际筛选)。我们将使用以下筛选器:

  • 静态路由器(可在其中指定将请求转发到哪个主机)。
  • 重写 URL
  • 连接(可在其中指定是否希望修改主机头部、HTTP 身份验证等等)。该筛选器向制定的主机发送请求,并将响应传回浏览器。

路径 /idp/fed 的完整策略如图 6 所示。

weber-oif-oag-fig06
图 6:OAG 中的 OIF 保护策略。

然而,要进行实际部署,还需要其他步骤:

  • 身份验证表单。IdP 将浏览器重定向到一个登录页面。在自带 LDAP 身份验证引擎的 OIF 中,该页面位于 /fed/user 下 — 一个 URI,其中上述配置没有路径。不过,可以使用筛选器 Static Router 和 Connect To URL 轻松添加这样一个路径。不检查内容就转发凭证发布。您可能希望添加一些筛选器来检查凭证发布是否仅包含用户名和口令(以及一个产品特定的 refld)。这非常容易,留给读者自己完成。
  • 一次性注销。上述策略假定每个 /fed/idp 访问都包含一个 SAML 请求。但可能并不总是这样。基于 SAML的注销也可以使用 /fed/idp。在本例中,必须增强上述策略来解释注销。这也非常容易实现。

使用 Oracle API Gateway 降低风险

灵活的攻击响应

使用 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 策略只需稍作修改。消息流如下:

  1. 用户(浏览器)通过 HTTP GET 发送 SAMLReq。
  2. OAG 截获请求并强制用户在 Access Manager 上进行身份验证。
  3. 用户在 Access Manager 上进行身份验证,然后 OAG 将 SAMLReq 传递给 OIF。
  4. OIF 检查用户是否在 Access Manager 上经过身份验证(此处就是这种情况),然后回复 SAMLReq。

注意,仅当 IdP 实现过程中出现攻击时(例如,通过 CERT 新闻),才需要这个备用设置。这是计划 B。将 OIF 重新配置为使用 Access Manager 作为身份验证引擎只需要几分钟。重新部署这个增强的策略,这将检查经过身份验证的用户,只需几分钟时间。

另一种方法是将带一个可选的 webgate 的反向代理 (OHS)(作为计划 B 的一部分启用)放到 OAG 前端(同一台服务器上),并在此终止 SSL 流量(图 7)。

weber-oif-oag-fig07
图 7:多元化安全性的替代配置

这将允许您解释两件事:

  • OAG SSL 实现中的错误,以及
  • IdP SAML 服务实现中的错误。

这将需要(作为计划 B 的一部):

  • 重启包括 SSL 模块和 webgate 的反向代理,
  • 在 API 网关处禁用 SSL。

同样,这还意味着可在几分钟内对这类攻击做出反应。不是太糟糕,对不对?

参考资料

附录

SAMLRequest 解压缩和 Base64 解码

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 多年的高可扩展性和身份管理系统工作。