Skip to content

fix(gateway): prevent browser rate-limit self-DoS on missing credentials#38725

Closed
ademczuk wants to merge 2 commits intoopenclaw:mainfrom
ademczuk:fix/gateway-auth-rate-limit-v2
Closed

fix(gateway): prevent browser rate-limit self-DoS on missing credentials#38725
ademczuk wants to merge 2 commits intoopenclaw:mainfrom
ademczuk:fix/gateway-auth-rate-limit-v2

Conversation

@ademczuk
Copy link
Copy Markdown
Contributor

@ademczuk ademczuk commented Mar 7, 2026

Summary

  • Problem: Opening the web UI without ?token= triggers a self-DoS: missing credentials count as rate-limit failures, and the browser auto-reconnects, exhausting the limit in ~40 seconds, leading to a 5-minute lockout for ALL browser connections from that IP.
  • What changed: (1) auth.ts - stop calling recordFailure() for token_missing/password_missing; only actual mismatches burn rate-limit slots. (2) gateway.ts - stop auto-reconnecting on non-recoverable auth errors (AUTH_TOKEN_MISSING, AUTH_PASSWORD_MISSING, AUTH_PASSWORD_MISMATCH, AUTH_RATE_LIMITED).
  • What did NOT change: browserRateLimiter config, rate limiter internals, device-token fallback flow (AUTH_TOKEN_MISMATCH deliberately NOT in the non-recoverable list, since the stale device-token to shared-token retry flow needs reconnect).

Continuation of #36138 (closed due to unrelated changes drifting into the branch). This PR contains only the gateway auth fix, the same 2 source files that received a 4/5 from Greptile with "No issues found."

Changes at a glance

File Change
src/gateway/auth.ts:L45-L52 Remove recordFailure() for token_missing/password_missing
ui/src/ui/gateway.ts:L1-L30 Add isNonRecoverableAuthError() gate before scheduleReconnect()
src/gateway/auth.test.ts 4 new tests: missing creds don't burn slots, mismatches do
src/gateway/reconnect-gating.test.ts 8 new tests: non-recoverable errors block reconnect, mismatch allows it

Change Type

  • Bug fix
  • Security hardening

Linked Issue

Fixes #28997

Verification

pnpm vitest run src/gateway/auth.test.ts        # 35 tests pass
pnpm vitest run src/gateway/reconnect-gating    # 8 tests pass

Risks and Mitigations

Risk Mitigation
No-token connections not rate-limited Missing-token connections are rejected immediately with no server cost. Real brute-force uses wrong tokens (mismatch), which remain rate-limited.
Non-recoverable error list needs future updating List is conservative, covering only errors that provably cannot resolve without user action. New codes default to allowing reconnect (safe default).

@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime size: S labels Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 7, 2026

Greptile Summary

This PR successfully fixes a self-DoS bug where opening the web UI without credentials would exhaust the browser rate-limit and lock out all connections from that IP. The fix is well-scoped and applied in two complementary layers: the server stops counting absent credentials as rate-limit failures, and the browser client stops auto-reconnecting when it receives an auth error that cannot resolve without user action.

Key strengths:

  • The recordFailure() removal for token_missing/password_missing is safe — the rate-limit check still runs first, so an already-exhausted IP is still rejected before credentials are even inspected.
  • The AUTH_TOKEN_MISMATCH exclusion from the non-recoverable list is correctly intentional: the stale device-token → clear → retry fallback loop requires at least one reconnect, and the rate limiter bounds the total number of retries before AUTH_RATE_LIMITED stops the loop.
  • pendingConnectError capture/reset ordering in the close handler is correct: the error is snapshotted into connectError before the field is cleared, so the isNonRecoverableAuthError gate always sees the intended value.
  • Test coverage is thorough for the happy and sad paths of both changes.

Confidence Score: 5/5

  • Safe to merge — minimal, targeted changes with no logic errors found.
  • No bugs or security regressions identified. Both changes (server-side recordFailure removal and client-side reconnect gating) are logically sound, the interaction between them is correctly designed, and the test suite covers the critical decision boundaries.
  • No files require special attention.

Last reviewed commit: 0e7ce3a

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e7ce3ab64

ℹ️ 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".

Comment thread ui/src/ui/gateway.ts
@ademczuk ademczuk force-pushed the fix/gateway-auth-rate-limit-v2 branch from 0e7ce3a to a2c70fe Compare March 7, 2026 18:32
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Mar 7, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Rate-limit bypass for missing gateway credentials enables unthrottled auth attempts (DoS)

1. 🟡 Rate-limit bypass for missing gateway credentials enables unthrottled auth attempts (DoS)

Property Value
Severity Medium
CWE CWE-307
Location src/gateway/auth.ts:404-463

Description

authorizeGatewayConnect() calls rateLimiter.check() but the limiter only tracks/blocks clients when recordFailure() is invoked. This change removed recordFailure() for missing token/password cases, so an attacker can repeatedly send unauthenticated connect attempts with absent credentials and never consume rate-limit capacity.

Why this is a practical bypass:

  • authorizeGatewayConnect() performs limiter.check(ip, scope) before validating credentials.
  • createAuthRateLimiter().check() is non-consuming; it only inspects stored failures.
  • Without recordFailure() on token_missing / password_missing, the limiter state never changes, so check() keeps returning allowed: true.
  • Result: unlimited connection/auth attempts that always fail with token_missing/password_missing but are never throttled, enabling request floods/log spam/CPU burn (DoS).

Vulnerable code (missing-credential early return without recording a failure):

if (!connectAuth?.token) {// ...
  return { ok: false, reason: "token_missing" };
}

A similar branch exists for password_missing.

Relevant limiter behavior:

  • check() does not increment attempts.
  • Only recordFailure() appends a timestamp and enforces lockout once maxAttempts is reached.

Recommendation

Treat missing credentials as rate-limit relevant events or introduce a separate limiter for "missing credential" floods.

Option A (simplest): record failures for missing credentials too:

if (!connectAuth?.token) {
  limiter?.recordFailure(ip, rateLimitScope);
  return { ok: false, reason: "token_missing" };
}

if (!password) {
  limiter?.recordFailure(ip, rateLimitScope);
  return { ok: false, reason: "password_missing" };
}

Option B (better UX): use a separate scope and/or higher threshold for missing credentials (to avoid locking out legitimate misconfigured clients while still preventing floods):

const missingScope = `${rateLimitScope}:missing`;
if (!connectAuth?.token) {
  limiter?.recordFailure(ip, missingScope);
  return { ok: false, reason: "token_missing" };
}

Also consider adding a lightweight per-IP connection attempt limiter at the WebSocket/HTTP entrypoint to protect CPU/logs regardless of auth result.


Analyzed PR: #38725 at commit b15e4ca

Last updated on: 2026-03-07T22:48:18Z

@ademczuk
Copy link
Copy Markdown
Contributor Author

ademczuk commented Mar 7, 2026

@Takhoffman - this could use a review when you've got a moment.

Context: Fixes a browser rate-limit self-DoS where missing auth credentials trigger recordFailure() on every auto-reconnect, locking the user out for 5 minutes. S-sized PR, 4 files, all CI green.

The "Changes at a glance" table in the description has the exact lines to review.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2c70fe6e4

ℹ️ 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".

Comment thread ui/src/ui/gateway.ts
@ademczuk ademczuk force-pushed the fix/gateway-auth-rate-limit-v2 branch 4 times, most recently from 6aaf200 to b6bb58d Compare March 7, 2026 20:59
@ademczuk ademczuk force-pushed the fix/gateway-auth-rate-limit-v2 branch from b6bb58d to 1d84b43 Compare March 7, 2026 21:24
@ademczuk
Copy link
Copy Markdown
Contributor Author

ademczuk commented Mar 7, 2026

Rebased onto latest main (70 commits forward). No content changes - same single commit, just synced with upstream.

The check CI failure is a pre-existing upstream issue (chatStreamSegments duplicate identifier in app-view-state.ts and type errors in client-fetch.loopback-auth.test.ts). Not related to this PR.

Stop counting missing credentials (token_missing, password_missing) as
rate-limit failures — a misconfigured browser is not a brute-force attack.
Stop auto-reconnecting on non-recoverable auth errors (missing token,
missing password, wrong password, rate-limited). Preserve device-token
fallback by allowing reconnect on token mismatch.

Includes unit tests for both fixes (12 new tests, all passing).

Continuation of openclaw#36138 (closed due to unrelated changes drifting in).
@ademczuk ademczuk force-pushed the fix/gateway-auth-rate-limit-v2 branch from 1d84b43 to 533ff3e Compare March 7, 2026 22:07
@ademczuk
Copy link
Copy Markdown
Contributor Author

ademczuk commented Mar 7, 2026

Valid concern, but the trade-off is intentional.

Missing credentials carry zero brute-force risk - there's nothing to guess. The real-world problem this PR fixes is more severe: a single misconfigured browser triggers recordFailure() on every auto-reconnect (~10 in 40 seconds), locking out ALL browser connections from that IP for 5 minutes, including properly-configured ones. That's a self-DoS from normal usage.

The existing isRateLimited() check still runs first (auth.ts:430-435), so any IP already locked out from credential mismatches stays blocked. We only remove the failure count for absent credentials, not wrong ones.

Your separate-scope suggestion is reasonable for defence in depth - worth exploring as a follow-up. The connection-level rate limiting in server.impl.ts (the browserRateLimiter with hardcoded exemptLoopback: false) still applies to all browser connections regardless of auth outcome, so there's still a connection-attempt cap independent of the auth rate limiter.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 533ff3e70b

ℹ️ 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".

Comment thread src/gateway/auth.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b15e4ca736

ℹ️ 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".

Comment thread ui/src/ui/gateway.ts
steipete added a commit that referenced this pull request Mar 7, 2026
Source: #38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 7, 2026

Landed on main in 3a74dc0.

Source commit from this PR: 533ff3e

What I kept:

  • stop counting token_missing / password_missing as auth rate-limit failures
  • stop auto-reconnecting Control UI clients on non-recoverable auth errors
  • regression coverage in src/gateway/auth.test.ts and new src/gateway/reconnect-gating.test.ts
  • changelog entry with contributor + PR reference

Gate run before landing:

  • pnpm lint
  • pnpm build
  • pnpm test

Thanks @ademczuk.

@steipete steipete closed this Mar 7, 2026
@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 7, 2026

Landed on main after maintainer squash/rebase.

Thanks @ademczuk.

@ademczuk
Copy link
Copy Markdown
Contributor Author

ademczuk commented Mar 7, 2026

Thanks for landing this one. Glad the rate-limit exemption and reconnect gating made the cut.

@ademczuk ademczuk deleted the fix/gateway-auth-rate-limit-v2 branch March 7, 2026 22:59
vincentkoc pushed a commit to BryanTegomoh/openclaw-upstream that referenced this pull request Mar 8, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
ziomancer pushed a commit to ziomancer/openclaw that referenced this pull request Mar 8, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
openperf pushed a commit to openperf/moltbot that referenced this pull request Mar 8, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
mcaxtr pushed a commit to mcaxtr/openclaw that referenced this pull request Mar 8, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
GordonSH-oss pushed a commit to GordonSH-oss/openclaw that referenced this pull request Mar 9, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 21, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
(cherry picked from commit 3a74dc0)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 21, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
(cherry picked from commit 3a74dc0)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 21, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
(cherry picked from commit 3a74dc0)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 21, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
(cherry picked from commit 3a74dc0)
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Source: openclaw#38725 / 533ff3e by @ademczuk.
Thanks @ademczuk.

Co-authored-by: ademczuk <andrew.demczuk@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Control UI webchat loses auth token on WebSocket reconnect, triggers rate limiter

2 participants