GHSA-g2qj-prgh-4g9r
Nhost Leaks Refresh Tokens via URL Query Parameter in OAuth Provider Callback
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| Go | github.com/nhost/nhost | < 0.0.0-20260330133707-294954e0fc3a | 0.0.0-20260330133707-294954e0fc3a |
Details
As written in the advisory (Markdown source, shown as plain text).
# Refresh Token Leaked via URL Query Parameter in OAuth Provider Callback
## Summary
The auth service's OAuth provider callback flow places the refresh token directly into the redirect URL as a query parameter. Refresh tokens in URLs are logged in browser history, server access logs, HTTP Referer headers, and proxy/CDN logs.
Note that the refresh token is one-time use and all of these leak vectors are on owned infrastructure or services integrated by the application developer.
## Affected Component
- **Repository**: `github.com/nhost/nhost`
- **Service**: `services/auth`
- **File**: `services/auth/go/controller/sign_in_provider_callback_get.go`
- **Function**: `signinProviderProviderCallback` (lines 257-261)
## Root Cause
In `sign_in_provider_callback_get.go:257-261`, after successful OAuth sign-in, the refresh token is appended as a URL query parameter:
```go
if session != nil {
values := redirectTo.Query()
values.Add("refreshToken", session.RefreshToken)
redirectTo.RawQuery = values.Encode()
}
```
This results in a redirect like:
```
HTTP/1.1 302 Found
Location: https://myapp.com/callback?refreshToken=a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
## Proof of Concept
### Step 1: Initiate OAuth login
```
GET /signin/provider/github?redirectTo=https://myapp.com/callback
```
### Step 2: Complete OAuth flow with provider
### Step 3: Auth service redirects with token in URL
```
HTTP/1.1 302 Found
Location: https://myapp.com/callback?refreshToken=a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
### Step 4: Token is now visible in owned infrastructure and services:
**Browser History:**
```
# User's browser history now contains the refresh token
```
**HTTP Referer Header:**
```
# If the callback page loads ANY external resource (image, script, etc.):
GET /resource.js HTTP/1.1
Host: cdn.example.com
Referer: https://myapp.com/callback?refreshToken=a1b2c3d4-e5f6-...
# Note: modern browsers default to strict-origin-when-cross-origin policy,
# which strips query parameters from cross-origin Referer headers.
# Additionally, the Referer is only sent to services integrated by the
# application developer (analytics, CDNs, etc.), not arbitrary third parties.
```
**Server Access Logs:**
```
# Reverse proxy, CDN, or load balancer logs on owned infrastructure:
2026-03-08 12:00:00 GET /callback?refreshToken=a1b2c3d4-e5f6-... 200
```
### Step 5: Attacker uses stolen refresh token
```bash
# Exchange stolen refresh token for new access token
curl -X POST https://auth.nhost.run/v1/token \
-H 'Content-Type: application/json' \
-d '{"refreshToken": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}'
# Note: refresh tokens are one-time use, so this only works if the
# legitimate client has not already consumed the token and if the attacker has
# compromised your infrastructure to get access to this information
```
## Impact
1. **Session Hijacking**: Anyone who obtains the token before it is consumed by the legitimate client can generate new access tokens, though the refresh token is one-time use and cannot be reused after consumption.
2. **Leak Vectors**: URL query parameters are visible in owned infrastructure and integrated services:
- Browser history (local access)
- HTTP Referer headers (mitigated by modern browser default referrer policies; only sent to developer-integrated services)
- Server access logs (owned infrastructure)
- Proxy/CDN/WAF logs (owned infrastructure)
3. **Affects All OAuth Providers**: Every OAuth provider flow (GitHub, Google, Apple, etc.) goes through the same callback handler.
## Fix
Implemented PKCE (Proof Key for Code Exchange) for the OAuth flow. With PKCE, the authorization code cannot be exchanged without the `code_verifier` that only the original client possesses, preventing token misuse even if the URL is logged.
See: https://docs.nhost.io/products/auth/pkce/
## Resources
- OWASP: Session Management - Token Transport: "Session tokens should not be transported in the URL"
- RFC 6749 Section 10.3: "Access tokens and refresh tokens MUST NOT be included in the redirect URI"
- CWE-598: Use of GET Request Method With Sensitive Query Strings
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
References
External links from the advisory. They lead to third-party sites.