Skip to content

Commit 84dd9c7

Browse files
committed
fix(gateway): fail closed for trusted-proxy auth
1 parent 97d2d40 commit 84dd9c7

3 files changed

Lines changed: 12 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ Docs: https://docs.openclaw.ai
156156
### Fixes
157157

158158
- Providers: preserve non-OK `text/event-stream` response bodies so provider HTTP errors keep their JSON detail instead of collapsing to generic streaming failures. Fixes #78180.
159+
- Gateway/auth: make explicit `trusted-proxy` mode fail closed instead of accepting local password fallback credentials after trusted-proxy identity checks fail. Fixes #78684.
159160
- Tools/session status: render the active heartbeat/run model for `session_status({"sessionKey":"current"})` instead of falling back to the persisted session default. Fixes #77493.
160161
- Doctor/secrets: allow safe inherited exec SecretRef `passEnv` names such as `HOME` while still blocking dangerous runtime env hooks. Fixes #78216.
161162
- Chat commands: make `/model default` reset the session model override instead of treating it as a literal model name. Fixes #78182.

src/gateway/auth.test.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -964,35 +964,35 @@ describe("trusted-proxy auth", () => {
964964
expect(res.reason).toBe("trusted_proxy_loopback_source");
965965
});
966966

967-
it("accepts local-direct password fallback when trusted-proxy auth fails", async () => {
967+
it("rejects local-direct password credentials when trusted-proxy auth fails", async () => {
968968
const limiter = createLimiterSpy();
969969
const res = await authorizeLocalDirect({
970970
password: "local-password", // pragma: allowlist secret
971971
connectPassword: "local-password", // pragma: allowlist secret
972972
rateLimiter: limiter,
973973
});
974974

975-
expect(res).toEqual({ ok: true, method: "password" });
976-
expect(limiter.check).toHaveBeenCalledWith("127.0.0.1", "shared-secret");
977-
expect(limiter.reset).toHaveBeenCalledWith("127.0.0.1", "shared-secret");
975+
expect(res).toEqual({ ok: false, reason: "trusted_proxy_loopback_source" });
976+
expect(limiter.check).not.toHaveBeenCalled();
977+
expect(limiter.reset).not.toHaveBeenCalled();
978978
expect(limiter.recordFailure).not.toHaveBeenCalled();
979979
});
980980

981-
it("rejects wrong local-direct password fallback and records the failure", async () => {
981+
it("ignores wrong local-direct password credentials when trusted-proxy auth fails", async () => {
982982
const limiter = createLimiterSpy();
983983
const res = await authorizeLocalDirect({
984984
password: "local-password", // pragma: allowlist secret
985985
connectPassword: "wrong-password", // pragma: allowlist secret
986986
rateLimiter: limiter,
987987
});
988988

989-
expect(res).toEqual({ ok: false, reason: "password_mismatch" });
990-
expect(limiter.check).toHaveBeenCalledWith("127.0.0.1", "shared-secret");
991-
expect(limiter.recordFailure).toHaveBeenCalledWith("127.0.0.1", "shared-secret");
989+
expect(res).toEqual({ ok: false, reason: "trusted_proxy_loopback_source" });
990+
expect(limiter.check).not.toHaveBeenCalled();
991+
expect(limiter.recordFailure).not.toHaveBeenCalled();
992992
expect(limiter.reset).not.toHaveBeenCalled();
993993
});
994994

995-
it("enforces rate-limit lockout before local-direct password fallback", async () => {
995+
it("does not apply shared-secret rate limits to trusted-proxy failures", async () => {
996996
const limiter = createLimiterSpy();
997997
limiter.check.mockReturnValueOnce({
998998
allowed: false,
@@ -1006,12 +1006,8 @@ describe("trusted-proxy auth", () => {
10061006
rateLimiter: limiter,
10071007
});
10081008

1009-
expect(res).toEqual({
1010-
ok: false,
1011-
reason: "rate_limited",
1012-
rateLimited: true,
1013-
retryAfterMs: 2500,
1014-
});
1009+
expect(res).toEqual({ ok: false, reason: "trusted_proxy_loopback_source" });
1010+
expect(limiter.check).not.toHaveBeenCalled();
10151011
expect(limiter.recordFailure).not.toHaveBeenCalled();
10161012
expect(limiter.reset).not.toHaveBeenCalled();
10171013
});

src/gateway/auth.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -466,26 +466,6 @@ async function authorizeGatewayConnectCore(
466466
}
467467
return { ok: true, method: "trusted-proxy", user: result.user };
468468
}
469-
if (localDirect && auth.password && connectAuth?.password) {
470-
if (limiter) {
471-
const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope);
472-
if (!rlCheck.allowed) {
473-
return {
474-
ok: false,
475-
reason: "rate_limited",
476-
rateLimited: true,
477-
retryAfterMs: rlCheck.retryAfterMs,
478-
};
479-
}
480-
}
481-
return authorizePasswordAuth({
482-
authPassword: auth.password,
483-
connectPassword: connectAuth.password,
484-
limiter,
485-
ip,
486-
rateLimitScope,
487-
});
488-
}
489469
return { ok: false, reason: result.reason };
490470
}
491471

0 commit comments

Comments
 (0)