fix(web_server): hold _oauth_sessions_lock during PKCE session state writes

_submit_anthropic_pkce() retrieved sess under _oauth_sessions_lock but
wrote back to sess["status"] and sess["error_message"] outside the lock.
A concurrent session GC or cancel could race with these writes, producing
inconsistent session state.

Wrap all 4 sess write sites in _oauth_sessions_lock:
- network exception path (Token exchange failed)
- missing access_token path
- credential save failure path
- success path (approved)
This commit is contained in:
sprmn24 2026-04-25 00:48:55 +03:00 committed by Teknium
parent fd3864d8bd
commit 7957da7a1d

View file

@ -1533,6 +1533,7 @@ def _submit_anthropic_pkce(session_id: str, code_input: str) -> Dict[str, Any]:
with urllib.request.urlopen(req, timeout=20) as resp: with urllib.request.urlopen(req, timeout=20) as resp:
result = json.loads(resp.read().decode()) result = json.loads(resp.read().decode())
except Exception as e: except Exception as e:
with _oauth_sessions_lock:
sess["status"] = "error" sess["status"] = "error"
sess["error_message"] = f"Token exchange failed: {e}" sess["error_message"] = f"Token exchange failed: {e}"
return {"ok": False, "status": "error", "message": sess["error_message"]} return {"ok": False, "status": "error", "message": sess["error_message"]}
@ -1541,6 +1542,7 @@ def _submit_anthropic_pkce(session_id: str, code_input: str) -> Dict[str, Any]:
refresh_token = result.get("refresh_token", "") refresh_token = result.get("refresh_token", "")
expires_in = int(result.get("expires_in") or 3600) expires_in = int(result.get("expires_in") or 3600)
if not access_token: if not access_token:
with _oauth_sessions_lock:
sess["status"] = "error" sess["status"] = "error"
sess["error_message"] = "No access token returned" sess["error_message"] = "No access token returned"
return {"ok": False, "status": "error", "message": sess["error_message"]} return {"ok": False, "status": "error", "message": sess["error_message"]}
@ -1549,9 +1551,11 @@ def _submit_anthropic_pkce(session_id: str, code_input: str) -> Dict[str, Any]:
try: try:
_save_anthropic_oauth_creds(access_token, refresh_token, expires_at_ms) _save_anthropic_oauth_creds(access_token, refresh_token, expires_at_ms)
except Exception as e: except Exception as e:
with _oauth_sessions_lock:
sess["status"] = "error" sess["status"] = "error"
sess["error_message"] = f"Save failed: {e}" sess["error_message"] = f"Save failed: {e}"
return {"ok": False, "status": "error", "message": sess["error_message"]} return {"ok": False, "status": "error", "message": sess["error_message"]}
with _oauth_sessions_lock:
sess["status"] = "approved" sess["status"] = "approved"
_log.info("oauth/pkce: anthropic login completed (session=%s)", session_id) _log.info("oauth/pkce: anthropic login completed (session=%s)", session_id)
return {"ok": True, "status": "approved"} return {"ok": True, "status": "approved"}