fix(auth): clear stale lockout state when user re-authenticates#43450
fix(auth): clear stale lockout state when user re-authenticates#43450ademczuk merged 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR adds a
Confidence Score: 2/5
Last reviewed commit: cc5f6ff |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc5f6ff211
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
375d7da to
6546e08
Compare
|
Re: Aisle (CWE-841 lockout bypass) By design. A user explicitly running Re: Greptile (2/5, two findings) Both findings addressed in 88b3fb1: wrapped in try/catch so corrupt store files can't block re-auth, and switched to |
854dd63 to
5350791
Compare
5350791 to
e91ad4d
Compare
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🔵 Potential local DoS via unbounded per-profile disk I/O when clearing auth lockouts on login
Description
While this is reasonable for normal installations (few profiles), the implementation can become very slow when the auth store contains many profiles (e.g., a corrupted/hand-edited
Vulnerable code (login path loop): const profileIds = listProfilesForProvider(store, provider);
for (const profileId of profileIds) {
await clearAuthProfileCooldown({ store, profileId, agentDir });
}Supporting sink (per-profile locked update + save): const updated = await updateAuthProfileStoreWithLock({ agentDir, updater: ... });
...
saveAuthProfileStore(store, agentDir);Impact:
RecommendationMitigate worst-case cost by clearing provider lockouts in one locked transaction (single read/lock + single write), and/or bounding work:
Example (single lock + single save): import { updateAuthProfileStoreWithLock } from "../../agents/auth-profiles/store.js";
import { resetUsageStats } from "../../agents/auth-profiles/usage.js";
await updateAuthProfileStoreWithLock({
agentDir,
updater: (freshStore) => {
const providerKey = normalizeProviderIdForAuth(provider);
let changed = false;
for (const [profileId, cred] of Object.entries(freshStore.profiles)) {
if (normalizeProviderIdForAuth(cred.provider) !== providerKey) continue;
if (!freshStore.usageStats?.[profileId]) continue;
updateUsageStatsEntry(freshStore, profileId, (e) => resetUsageStats(e));
changed = true;
}
return changed;
},
});This reduces the operation to O(N) in-memory updates with a single lock and write, preventing large N from causing excessive I/O and lock contention. 2. 🔵 Prototype pollution via proto/constructor keys in auth profile store JSON parsing
DescriptionThe auth profile store loader copies user-controlled JSON object keys directly into plain JavaScript objects. Special keys like This occurs while coercing
If an attacker can cause a crafted Vulnerable code: const normalized: Record<string, AuthProfileCredential> = {};
for (const [key, value] of Object.entries(profiles)) {
...
normalized[key] = parsed.credential;
}RecommendationHarden JSON-to-object coercion against prototype pollution. Options:
const normalized: Record<string, AuthProfileCredential> = Object.create(null);
for (const [key, value] of Object.entries(profiles)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
...
normalized[key] = parsed.credential;
}
Also apply the same hardening to other dictionaries derived from JSON ( Analyzed PR: #43450 at commit Last updated on: 2026-03-14T07:18:27Z |
|
Re: Aisle CWE-307 (pre-auth lockout reset) The lockout is already stale by design here. When a user explicitly runs `openclaw models auth login`, they're telling us their credentials need fixing. The lockout state they're bypassing was set because those same credentials failed - keeping it around while they're actively trying to fix it doesn't add protection, it blocks recovery. If the user cancels the OAuth flow mid-way, the worst case is: lockout state is cleared but credentials are still broken, so the next API call fails and the lockout gets re-applied immediately. The circuit breaker re-engages on the next failure. Moving the clear to post-auth would mean a user with a corrupted `disabledReason: "auth_permanent"` entry (the bug this PR fixes) can't even start the login flow on some provider plugin paths where the disabled check fires before the OAuth redirect. The silent `catch {}` is intentional - lockout clearing is best-effort housekeeping. Blocking re-authentication on a lockout-clearing failure would be worse than the CWE-307 risk. |
c6b5bfc to
af8fdf9
Compare
|
Rebased onto latest main. CHANGELOG entry verified. All code checks, extensions, and protocol pass. The node test shard and Windows shard failures are from the |
af8fdf9 to
db27ab2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db27ab28e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
db27ab2 to
b7c3bb8
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b7c3bb8565
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer. Uses the agent-scoped store (`loadAuthProfileStoreForRuntime`) for correct multi-agent profile resolution and wraps the housekeeping in try/catch so corrupt store files never block re-authentication. Fixes openclaw#43057
oxlint no-unnecessary-type-assertion: invocationCallOrder[0] already returns number, not number | undefined.
b7c3bb8 to
74ebe65
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74ebe65571
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| */ | ||
| async function clearStaleProfileLockouts(provider: string, agentDir: string): Promise<void> { | ||
| try { | ||
| const store = loadAuthProfileStoreForRuntime(agentDir); |
There was a problem hiding this comment.
Load lockout precheck store in read-only mode
clearStaleProfileLockouts calls loadAuthProfileStoreForRuntime(agentDir) before any authentication work begins, but this loader is write-enabled by default, so in non-main agents without an auth store it can persist a cloned auth-profiles.json from main during the precheck; as a result, openclaw models auth login may create/modify auth storage even when the user cancels or OAuth fails. Fresh evidence: loadAuthProfileStoreForAgent performs saveJsonFile(authPath, mainStore) when agentDir && !readOnly and the subagent file is missing (src/agents/auth-profiles/store.ts:391-400).
Useful? React with 👍 / 👎.
Summary
When an OAuth access token is revoked or expires server-side while the local
expirestimestamp is still in the future, the agent marks the profile asauth_permanent(disabled for 5-24 hours with exponential backoff). Runningopenclaw models auth loginto fix this doesn't clear the stale lockout state, so the user remains locked out until the timer expires.This adds a
clearStaleProfileLockouts()call inmodelsAuthLoginCommandthat clearsdisabledUntil,disabledReason,cooldownUntil,errorCount, andfailureCountsfor all profiles matching the target provider before attempting the new login flow. Covers both the built-in OpenAI Codex path and the plugin provider path.Changes
src/commands/models/auth.ts: addclearStaleProfileLockouts()helper that loads the auth store, finds profiles for the provider vialistProfilesForProvider, and calls the existingclearAuthProfileCooldownfor each. Called in both theopenai-codexbranch and the generic plugin provider branch ofmodelsAuthLoginCommand.src/commands/models/auth.test.ts: add test verifying lockout state is cleared before login attempt (with call-order assertion). Fix stalegpt-5.3-codexmodel name in pre-existing tests.CHANGELOG.md: add entry under Fixes.Test plan
disabledUntil+disabledReason: "auth_permanent"is cleared beforeloginOpenAICodexOAuthis calledclearAuthProfileCooldowninvocation precedesloginOpenAICodexOAuthauth.test.tspasspnpm tsgo --noEmitclean (no new type errors)Fixes #43057