openclaw - ✅(Solved) Fix [Bug]: Gateway auth rate limiting is opt-in on non-loopback shared-secret deployments [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#64162Fetched 2026-04-11 06:16:10
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
closed ×1cross-referenced ×1

Error Message

The remaining protections are advisory rather than enforcing. src/security/audit.ts:657-667 emits gateway.auth_no_rate_limit as a warn finding when non-loopback auth has no limiter, and the docs describe gateway.auth.rateLimit as optional (docs/gateway/configuration-reference.md:2790-2793). Startup also auto-generates a 48-hex-character token when token mode has no configured secret (src/gateway/startup-auth.ts:252-257), and src/security/audit.ts:587-595 warns on tokens shorter than 24 characters. Those mitigations raise attack complexity for default token deployments, which is why this finding is hardening rather than a direct auth bypass. The concrete gap is that password-mode deployments and manually weakened-token deployments still accept unlimited online guesses on exposed gateway auth surfaces until operators opt into throttling.

Root Cause

No documented trust boundary is bypassed in the verified code path: SECURITY.md says authenticated Gateway callers are treated as trusted operators, and the missing default limiter does not authenticate an attacker by itself. docs/gateway/security/index.md already classifies the same condition as the gateway.auth_no_rate_limit audit warning, so this is a defense-in-depth gap on the shared-secret auth boundary rather than a direct security-specific auth bypass. It is not covered by the Out of Scope list because the finding does not rely on prompt injection, hostile multi-tenant assumptions, or attacker control of trusted local state; it concerns a first-party exposed auth surface that omits online-guess throttling unless operators opt in.

Impact

When gateway.bind is non-loopback and shared-secret auth is in use, failed token/password attempts are throttled only if the operator explicitly configures gateway.auth.rateLimit. Without that config key, HTTP auth paths and non-browser WebSocket handshakes stay on unlimited 401/unauthorized responses instead of escalating to temporary 429 lockouts, so exposed deployments rely entirely on secret entropy rather than per-IP failed-auth throttling; on deployments using weak passwords or manually weakened tokens, a successful guess yields the same shared-secret operator access the gateway docs assign to token/password auth.

Fix Action

Fix / Workaround

The remaining protections are advisory rather than enforcing. src/security/audit.ts:657-667 emits gateway.auth_no_rate_limit as a warn finding when non-loopback auth has no limiter, and the docs describe gateway.auth.rateLimit as optional (docs/gateway/configuration-reference.md:2790-2793). Startup also auto-generates a 48-hex-character token when token mode has no configured secret (src/gateway/startup-auth.ts:252-257), and src/security/audit.ts:587-595 warns on tokens shorter than 24 characters. Those mitigations raise attack complexity for default token deployments, which is why this finding is hardening rather than a direct auth bypass. The concrete gap is that password-mode deployments and manually weakened-token deployments still accept unlimited online guesses on exposed gateway auth surfaces until operators opt into throttling.

PR fix notes

PR #64164: fix: Gateway auth rate limiting is opt-in on non-loopback...

Description (problem / solution / changelog)

Fix Summary

When gateway.bind is non-loopback and shared-secret auth is in use, failed token/password attempts are throttled only if the operator explicitly configures gateway.auth.rateLimit. Without that config key, HTTP auth paths and non-browser WebSocket handshakes stay on unlimited 401/unauthorized responses instead of escalating to temporary 429 lockouts, so exposed deployments rely entirely on secret entropy rather than per-IP failed-auth throttling; on deployments using weak passwords or manually weakened tokens, a successful guess yields the same shared-secret operator access the gateway docs assign to token/password auth.

Issue Linkage

Fixes #64162

Security Snapshot

  • CVSS v3.1: 3.7 (Low)
  • CVSS v4.0: 6.3 (Medium)

Implementation Details

Files Changed

  • docs/gateway/configuration-reference.md (+2/-1)
  • docs/gateway/openai-http-api.md (+1/-1)
  • src/gateway/openai-http.test.ts (+48/-0)
  • src/gateway/server.impl.ts (+28/-1)

Technical Analysis

  1. Configure an exposed gateway with shared-secret auth and no auth limiter, for example gateway.bind: "lan" plus gateway.auth.mode: "password" or a manually configured gateway.auth.token, while leaving gateway.auth.rateLimit unset. The configuration reference documents gateway.auth.rateLimit as optional at docs/gateway/configuration-reference.md:2790-2793.
  2. Send repeated wrong shared-secret attempts from the same remote IP to any gateway HTTP auth surface (/v1/chat/completions, /v1/responses, /tools/invoke) or to the WebSocket connect handshake without an Origin header. For WebSocket attempts, reconnect after each short burst because UnauthorizedFloodGuard is per connection.
  3. In this configuration, createGatewayAuthRateLimiters() returns rateLimiter: undefined, so the HTTP handlers and WebSocket handshake path call authorizeGatewayConnect() without a limiter. src/gateway/auth.ts:553-562 therefore never sets reason: "rate_limited", and src/gateway/http-common.ts:47-65 / src/gateway/server-http.ts:239-265 can emit only 401 unauthorized failures for wrong secrets on the main auth surface.
  4. Compare that with the explicit-config path in src/gateway/openai-http.test.ts:700-741: once gateway.auth.rateLimit is configured, the second bad request returns 429 with Retry-After. Without that config, the main shared-secret auth surface never transitions from repeated failures into lockout.

Validation Evidence

  • Command: pnpm test src/gateway/server-runtime-config.test.ts && pnpm test src/gateway/openai-http.test.ts -t "returns 429 for repeated failed auth on non-loopback shared-token binds by default"
  • Status: passed (with pre-existing baseline failures)

Risk and Compatibility

non-breaking; no known regression impact

AI-Assisted Disclosure

  • AI-assisted: yes
  • Model: opencode/claude-sonnet-4-6

Changed files

  • docs/gateway/configuration-reference.md (modified, +2/-1)
  • docs/gateway/openai-http-api.md (modified, +1/-1)
  • src/gateway/openai-http.test.ts (modified, +48/-0)
  • src/gateway/server.impl.ts (modified, +28/-1)

Code Example

function createGatewayAuthRateLimiters(rateLimitConfig: AuthRateLimitConfig | undefined): {
  rateLimiter?: AuthRateLimiter;
  browserRateLimiter: AuthRateLimiter;
} {
  const rateLimiter = rateLimitConfig ? createAuthRateLimiter(rateLimitConfig) : undefined;
  const browserRateLimiter = createAuthRateLimiter({
    ...rateLimitConfig,
    exemptLoopback: false,
  });
  return { rateLimiter, browserRateLimiter };
}

---

if (limiter) {
  const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope);
  if (!rlCheck.allowed) {
    return { ok: false, reason: "rate_limited", rateLimited: true, retryAfterMs: rlCheck.retryAfterMs };
  }
}

if (!safeEqualSecret(password, auth.password)) {
  limiter?.recordFailure(ip, rateLimitScope);
  return { ok: false, reason: "password_mismatch" };
}
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score3.7 / 10.06.3 / 10.0
SeverityLowMedium
VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:NCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
CalculatorCVSS v3.1 CalculatorCVSS v4.0 Calculator

Threat Model Alignment

Classification: hardening

No documented trust boundary is bypassed in the verified code path: SECURITY.md says authenticated Gateway callers are treated as trusted operators, and the missing default limiter does not authenticate an attacker by itself. docs/gateway/security/index.md already classifies the same condition as the gateway.auth_no_rate_limit audit warning, so this is a defense-in-depth gap on the shared-secret auth boundary rather than a direct security-specific auth bypass. It is not covered by the Out of Scope list because the finding does not rely on prompt injection, hostile multi-tenant assumptions, or attacker control of trusted local state; it concerns a first-party exposed auth surface that omits online-guess throttling unless operators opt in.

Impact

When gateway.bind is non-loopback and shared-secret auth is in use, failed token/password attempts are throttled only if the operator explicitly configures gateway.auth.rateLimit. Without that config key, HTTP auth paths and non-browser WebSocket handshakes stay on unlimited 401/unauthorized responses instead of escalating to temporary 429 lockouts, so exposed deployments rely entirely on secret entropy rather than per-IP failed-auth throttling; on deployments using weak passwords or manually weakened tokens, a successful guess yields the same shared-secret operator access the gateway docs assign to token/password auth.

Affected Component

File: src/gateway/server.impl.ts:208-218

function createGatewayAuthRateLimiters(rateLimitConfig: AuthRateLimitConfig | undefined): {
  rateLimiter?: AuthRateLimiter;
  browserRateLimiter: AuthRateLimiter;
} {
  const rateLimiter = rateLimitConfig ? createAuthRateLimiter(rateLimitConfig) : undefined;
  const browserRateLimiter = createAuthRateLimiter({
    ...rateLimitConfig,
    exemptLoopback: false,
  });
  return { rateLimiter, browserRateLimiter };
}

File: src/gateway/auth.ts:553-562,595-606

if (limiter) {
  const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope);
  if (!rlCheck.allowed) {
    return { ok: false, reason: "rate_limited", rateLimited: true, retryAfterMs: rlCheck.retryAfterMs };
  }
}

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

The optional rateLimiter is forwarded unchanged into both HTTP and WebSocket auth wiring at src/gateway/server.impl.ts:666-669,1343-1354. Browser-origin WebSocket handshakes switch to the separate always-on limiter in src/gateway/server/ws-connection/handshake-auth-helpers.ts:46-60; direct HTTP requests and non-browser WebSocket clients keep using the optional rateLimiter path instead.

Technical Reproduction

  1. Configure an exposed gateway with shared-secret auth and no auth limiter, for example gateway.bind: "lan" plus gateway.auth.mode: "password" or a manually configured gateway.auth.token, while leaving gateway.auth.rateLimit unset. The configuration reference documents gateway.auth.rateLimit as optional at docs/gateway/configuration-reference.md:2790-2793.
  2. Send repeated wrong shared-secret attempts from the same remote IP to any gateway HTTP auth surface (/v1/chat/completions, /v1/responses, /tools/invoke) or to the WebSocket connect handshake without an Origin header. For WebSocket attempts, reconnect after each short burst because UnauthorizedFloodGuard is per connection.
  3. In this configuration, createGatewayAuthRateLimiters() returns rateLimiter: undefined, so the HTTP handlers and WebSocket handshake path call authorizeGatewayConnect() without a limiter. src/gateway/auth.ts:553-562 therefore never sets reason: "rate_limited", and src/gateway/http-common.ts:47-65 / src/gateway/server-http.ts:239-265 can emit only 401 unauthorized failures for wrong secrets on the main auth surface.
  4. Compare that with the explicit-config path in src/gateway/openai-http.test.ts:700-741: once gateway.auth.rateLimit is configured, the second bad request returns 429 with Retry-After. Without that config, the main shared-secret auth surface never transitions from repeated failures into lockout.

Demonstrated Impact

browserRateLimiter is always instantiated, but it is selected only for handshakes that present an Origin header (src/gateway/server/ws-connection/handshake-auth-helpers.ts:46-60). Direct API clients, CLI clients, and attacker-controlled non-browser WebSocket connections use the optional rateLimiter instead. UnauthorizedFloodGuard in src/gateway/server/ws-connection/unauthorized-flood-guard.ts:18-57 only counts unauthorized frames on one live socket, and message-handler.ts:1357-1372 closes that socket after repeated failures; reconnecting starts a fresh counter. createPreauthConnectionBudget() in src/gateway/server/preauth-connection-budget.ts:1-58 only caps concurrent unauthenticated sockets per IP and does not slow repeated credential guesses over time.

The remaining protections are advisory rather than enforcing. src/security/audit.ts:657-667 emits gateway.auth_no_rate_limit as a warn finding when non-loopback auth has no limiter, and the docs describe gateway.auth.rateLimit as optional (docs/gateway/configuration-reference.md:2790-2793). Startup also auto-generates a 48-hex-character token when token mode has no configured secret (src/gateway/startup-auth.ts:252-257), and src/security/audit.ts:587-595 warns on tokens shorter than 24 characters. Those mitigations raise attack complexity for default token deployments, which is why this finding is hardening rather than a direct auth bypass. The concrete gap is that password-mode deployments and manually weakened-token deployments still accept unlimited online guesses on exposed gateway auth surfaces until operators opt into throttling.

Environment

Verified against OpenClaw release v2026.4.2 (45546b946068fe57a0248eb81bcb3be113f3919d, published 2026-04-02T18:30:45Z) by tracing src/gateway/server.impl.ts, src/gateway/auth.ts, the gateway HTTP/WS auth handlers, and the security audit/docs guidance in this workspace. Affected deployments are gateways reachable over non-loopback interfaces (lan, tailnet, or custom) using shared-secret token or password auth with gateway.auth.rateLimit unset.

Remediation Advice

Make failed-auth throttling the default for non-loopback shared-secret auth rather than an opt-in setting. Reuse the existing createAuthRateLimiter defaults for token/password gateway auth, keep explicit gateway.auth.rateLimit configuration for threshold tuning instead of basic enablement, and align runtime behavior with the existing audit guidance so exposed deployments cannot run indefinitely with unlimited online guesses.

<!-- submission-marker:AA-gbf-gateway-auth-rate-limit-brute-force -->

extent analysis

TL;DR

To mitigate the brute-force vulnerability, set a default rate limiter for non-loopback shared-secret auth in the gateway configuration.

Guidance

  1. Configure a default rate limiter: Set gateway.auth.rateLimit to a reasonable default value to enable failed-auth throttling for non-loopback shared-secret auth.
  2. Reuse existing createAuthRateLimiter defaults: Use the existing defaults for token/password gateway auth to ensure consistent behavior.
  3. Keep explicit configuration for threshold tuning: Allow operators to tune the rate limit threshold using the gateway.auth.rateLimit configuration.
  4. Align runtime behavior with audit guidance: Ensure that exposed deployments cannot run indefinitely with unlimited online guesses by enabling failed-auth throttling by default.

Example

// src/gateway/server.impl.ts
function createGatewayAuthRateLimiters(rateLimitConfig: AuthRateLimitConfig | undefined): {
  rateLimiter?: AuthRateLimiter;
  browserRateLimiter: AuthRateLimiter;
} {
  const defaultRateLimitConfig: AuthRateLimitConfig = {
    // Set default rate limit configuration
    maxAttempts: 5,
    retryAfterMs: 30000,
  };
  const rateLimiter = rateLimitConfig ? createAuthRateLimiter(rateLimitConfig) : createAuthRateLimiter(defaultRateLimitConfig);
  const browserRateLimiter = createAuthRateLimiter({
    ...rateLimitConfig,
    exemptLoopback: false,
  });
  return { rateLimiter, browserRateLimiter };
}

Notes

This solution assumes that the createAuthRateLimiter function is correctly implemented and that the AuthRateLimitConfig type has the necessary properties to configure the rate limiter.

Recommendation

Apply the workaround by setting a default rate limiter for non-loopback shared-secret auth to prevent brute-force attacks. This will ensure that exposed deployments have basic protection against unlimited online guesses.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING