How OAuth Token Binding Prevents Session Hijacking in Weibo Login Implementation
Introduction
In the Weibo OAuth login handler (trunk/web/login_weibo.php), developers correctly implemented CSRF protection by validating the state parameter. However, a critical gap existed in the authentication flow: the OAuth access token returned by Weibo was stored in the session without being bound to the session ID itself.
This means that if an attacker could intercept the access token via a man-in-the-middle (MITM) attack on an unencrypted HTTP connection or exploit an XSS vulnerability to steal the token from the callback response, they could replay that stolen token to authenticate as the victim user—even in a completely different session. The vulnerability bypassed the CSRF protection because it didn't protect the token itself from replay attacks.
The Vulnerability Explained
What's Happening in the Code?
Let's look at the vulnerable code flow in login_weibo.php:
// After receiving OAuth callback from Weibo
session_regenerate_id(true);
$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;
// token is now stored but NOT bound to this session
The problem is that the $token (OAuth access token) returned by Weibo's OAuth provider was being used for authentication but wasn't cryptographically bound to the session ID. This created a dangerous scenario:
- Attacker intercepts token: Via MITM on HTTP, the attacker captures the raw OAuth token in the Weibo callback response
- Attacker replays token: The attacker presents this same token to the application (or directly to Weibo's API), but in their own session
- Session hijacking succeeds: Because there's no binding requirement, the token validates successfully in the attacker's session, allowing them to authenticate as the victim
Real-World Attack Scenario
Imagine a user on a public WiFi network logging into your application via Weibo:
User → [HTTP callback: access_token=abc123xyz] → Attacker
(no HTTPS encryption, token visible in URL/response)
Attacker's action:
curl -H "Authorization: Bearer abc123xyz" https://app.com/api/user
→ Successfully retrieves victim's data in attacker's session
Even though the user authenticated properly and their session ID is different, the stolen token works because nothing validates that it belongs to that specific session.
Why CSRF Protection Isn't Enough
You might ask: "Didn't the state parameter prevent this?" The answer is no. The state parameter prevents attackers from initiating a login flow for the victim, but it doesn't protect the token itself from replay once it's been intercepted. These are two different threats:
- CSRF threat (prevented by state parameter): Attacker tricks victim into clicking a malicious link that initiates OAuth flow in victim's browser
- Token replay threat (NOT prevented by state parameter): Attacker intercepts the token itself and reuses it
The Fix
The fix introduces session-bound token validation by storing a cryptographic hash of the OAuth token combined with the session ID:
// After receiving OAuth callback from Weibo
session_regenerate_id(true);
$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;
// bind the oauth access token to this session so a stolen token
// cannot be replayed to hijack a different session
$_SESSION[$OJ_NAME . '_' . 'oauth_weibo_token'] = hash('sha256', $token . session_id());
How This Solves the Problem
- Token is bound to session: The hash of
$token . session_id()ensures this specific token can only be used with this specific session - Stolen token becomes useless: If an attacker steals the raw token, they can't reuse it because:
- They don't know the victim's session ID
- Even if they did, the hash would be different for their own session - Token validation on use: On subsequent API calls or requests, the application must verify that the stored hash matches
hash('sha256', $received_token . session_id())
Why SHA-256?
- Preimage resistant: An attacker can't reverse the hash to extract the session ID
- Collision resistant: The attacker can't generate a different token+session combination that produces the same hash
- Fast computation: Suitable for request-time validation
- Standard: SHA-256 is part of PHP's built-in
hash()function
Before and After Comparison
Before (Vulnerable):
$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;
// Token stored somewhere but not bound to session
// Any presentation of this token works in any session
After (Secure):
$_SESSION[$OJ_NAME . '_' . 'user_id'] = $uname;
$_SESSION[$OJ_NAME . '_' . 'oauth_weibo_token'] = hash('sha256', $token . session_id());
// Token now bound to this specific session
// Same token cannot be replayed in different sessions
Prevention & Best Practices
1. Always Bind Tokens to Sessions
For any OAuth flow, immediately bind the access token to the session:
// Good practice
$_SESSION['oauth_token_binding'] = hash('sha256', $access_token . session_id());
2. Validate Binding on Every OAuth Operation
Before using a stored token, verify the binding:
function validate_oauth_token($received_token) {
$expected_hash = hash('sha256', $received_token . session_id());
if ($_SESSION['oauth_token_binding'] !== $expected_hash) {
// Token doesn't match this session - reject it
session_destroy();
return false;
}
return true;
}
3. Use HTTPS for All OAuth Flows
Token binding helps, but HTTPS prevents token interception in the first place:
- Redirect URIs must use HTTPS
- Callback responses must be over HTTPS
- All API calls using the token must be over HTTPS
4. Regenerate Session After Authentication
The code correctly uses session_regenerate_id(true) to create a new session ID after successful OAuth authentication. This prevents session fixation attacks:
session_regenerate_id(true); // True = delete old session data
5. Implement Token Expiration
Store the token binding timestamp and validate expiration:
$_SESSION[$OJ_NAME . '_' . 'oauth_weibo_token'] = [
'hash' => hash('sha256', $token . session_id()),
'issued_at' => time(),
'ttl' => 3600 // 1 hour
];
6. Use Static Analysis to Detect This Pattern
Orbis AppSec and similar tools can identify OAuth tokens stored without session binding by detecting:
- Assignment of OAuth tokens to $_SESSION
- Missing hash/binding operations immediately after token storage
- OAuth handlers that don't validate token-to-session relationships
Key Takeaways
- Token binding is not optional: OAuth access tokens must be cryptographically bound to the session that received them, even when CSRF protection is in place
- Session ID + token hash: Using SHA-256(
$token . session_id()) creates a simple, effective binding that's tied to both the token and the specific session - CSRF protection ≠ token replay protection: The
stateparameter prevents unauthorized login initiation but doesn't protect intercepted tokens from being replayed - HTTPS + token binding creates defense-in-depth: HTTPS prevents token interception, and token binding ensures stolen tokens can't be reused in other sessions
- Public WiFi scenario is real: OAuth tokens in callback responses on HTTP are especially vulnerable; token binding is critical for this deployment model
How Orbis AppSec Detected This
Source: OAuth callback handler receives $token variable from Weibo's OAuth redirect response
Sink: The $token is stored in $_SESSION[$OJ_NAME . '_' . 'user_id'] context without cryptographic binding at line 77 of login_weibo.php
Missing control: No validation that the token is bound to the current session_id(). The token could be replayed in different sessions or by different attackers who intercept it.
CWE: CWE-384 (Session Fixation) and CWE-613 (Insufficient Session Expiration), as OAuth tokens can be used across session boundaries without binding
Fix: Immediately after storing the user ID in the session, bind the OAuth token to the session by storing hash('sha256', $token . session_id()) in the session. Validate this binding on every use of the token.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
OAuth implementations that properly handle CSRF tokens but fail to bind access tokens to sessions create a dangerous gap in authentication security. The fix in login_weibo.php demonstrates a simple but critical security pattern: every authentication token must be bound to the session that received it.
By adding just three lines of code—a SHA-256 hash of the token combined with the session ID—the fix prevents token replay attacks even if the token is intercepted via MITM or XSS. This is a key lesson for developers implementing OAuth flows: CSRF protection and token binding are complementary controls that work together to secure authentication.
For containerized services and applications exposed to public networks, this pattern is especially important. Implement session-bound token validation in your OAuth handlers today.
References
- CWE-384: Session Fixation
- CWE-613: Insufficient Session Expiration
- OWASP Session Management Cheat Sheet
- OWASP OAuth 2.0 Security Best Practices
- PHP session_regenerate_id() Documentation
- PHP hash() Function Documentation
- Semgrep Rule: Detect OAuth tokens in sessions without binding
- fix: add output encoding in login_weibo.php