Gateway: sanitize WebSocket log headers (#15592)

This commit is contained in:
Shadow
2026-02-13 11:11:54 -06:00
committed by GitHub
parent b3b49bed80
commit d637a26350
2 changed files with 36 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
- Agents/Image tool: cap image-analysis completion `maxTokens` by model capability (`min(4096, model.maxTokens)`) to avoid over-limit provider failures while still preventing truncation. (#11770) Thanks @detecti1.
- Security/Canvas: serve A2UI assets via the shared safe-open path (`openFileWithinRoot`) to close traversal/TOCTOU gaps, with traversal and symlink regression coverage. (#10525) Thanks @abdelsfane.
- Security/Gateway: breaking default-behavior change - canvas IP-based auth fallback now only accepts machine-scoped addresses (RFC1918, link-local, ULA IPv6, CGNAT); public-source IP matches now require bearer token auth. (#14661) Thanks @sumleo.
- Security/Gateway: sanitize and truncate untrusted WebSocket header values in pre-handshake close logs to reduce log-poisoning risk. Thanks @thewilloftheshadow.
- Security/WhatsApp: enforce `0o600` on `creds.json` and `creds.json.bak` on save/backup/restore paths to reduce credential file exposure. (#10529) Thanks @abdelsfane.
- Security/Gateway + ACP: block high-risk tools (`sessions_spawn`, `sessions_send`, `gateway`, `whatsapp_login`) from HTTP `/tools/invoke` by default with `gateway.tools.{allow,deny}` overrides, and harden ACP permission selection to fail closed when tool identity/options are ambiguous while supporting `allow_always`/`reject_always`. (#15390) Thanks @aether-ai-agent.
- Gateway/Tools Invoke: sanitize `/tools/invoke` execution failures while preserving `400` for tool input errors and returning `500` for unexpected runtime failures, with regression coverage and docs updates. (#13185) Thanks @davidrudduck.

View File

@@ -7,6 +7,7 @@ import type { GatewayRequestContext, GatewayRequestHandlers } from "../server-me
import type { GatewayWsClient } from "./ws-types.js";
import { resolveCanvasHostUrl } from "../../infra/canvas-host-url.js";
import { listSystemPresence, upsertPresence } from "../../infra/system-presence.js";
import { truncateUtf16Safe } from "../../utils.js";
import { isWebchatClient } from "../../utils/message-channel.js";
import { isLoopbackAddress } from "../net.js";
import { getHandshakeTimeoutMs } from "../server-constants.js";
@@ -17,6 +18,28 @@ import { attachGatewayWsMessageHandler } from "./ws-connection/message-handler.j
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
const LOG_HEADER_MAX_LEN = 300;
const LOG_HEADER_CONTROL_REGEX = /[\u0000-\u001f\u007f-\u009f]/g;
const LOG_HEADER_FORMAT_REGEX = /\p{Cf}/gu;
const sanitizeLogValue = (value: string | undefined): string | undefined => {
if (!value) {
return undefined;
}
const cleaned = value
.replace(LOG_HEADER_CONTROL_REGEX, " ")
.replace(LOG_HEADER_FORMAT_REGEX, " ")
.replace(/\s+/g, " ")
.trim();
if (!cleaned) {
return undefined;
}
if (cleaned.length <= LOG_HEADER_MAX_LEN) {
return cleaned;
}
return truncateUtf16Safe(cleaned, LOG_HEADER_MAX_LEN);
};
export function attachGatewayWsConnectionHandler(params: {
wss: WebSocketServer;
clients: Set<GatewayWsClient>;
@@ -156,6 +179,11 @@ export function attachGatewayWsConnectionHandler(params: {
socket.once("close", (code, reason) => {
const durationMs = Date.now() - openedAt;
const logForwardedFor = sanitizeLogValue(forwardedFor);
const logOrigin = sanitizeLogValue(requestOrigin);
const logHost = sanitizeLogValue(requestHost);
const logUserAgent = sanitizeLogValue(requestUserAgent);
const logReason = sanitizeLogValue(reason?.toString());
const closeContext = {
cause: closeCause,
handshake: handshakeState,
@@ -163,10 +191,10 @@ export function attachGatewayWsConnectionHandler(params: {
lastFrameType,
lastFrameMethod,
lastFrameId,
host: requestHost,
origin: requestOrigin,
userAgent: requestUserAgent,
forwardedFor,
host: logHost,
origin: logOrigin,
userAgent: logUserAgent,
forwardedFor: logForwardedFor,
...closeMeta,
};
if (!client) {
@@ -174,13 +202,13 @@ export function attachGatewayWsConnectionHandler(params: {
? logWsControl.debug
: logWsControl.warn;
logFn(
`closed before connect conn=${connId} remote=${remoteAddr ?? "?"} fwd=${forwardedFor ?? "n/a"} origin=${requestOrigin ?? "n/a"} host=${requestHost ?? "n/a"} ua=${requestUserAgent ?? "n/a"} code=${code ?? "n/a"} reason=${reason?.toString() || "n/a"}`,
`closed before connect conn=${connId} remote=${remoteAddr ?? "?"} fwd=${logForwardedFor || "n/a"} origin=${logOrigin || "n/a"} host=${logHost || "n/a"} ua=${logUserAgent || "n/a"} code=${code ?? "n/a"} reason=${logReason || "n/a"}`,
closeContext,
);
}
if (client && isWebchatClient(client.connect.client)) {
logWsControl.info(
`webchat disconnected code=${code} reason=${reason?.toString() || "n/a"} conn=${connId}`,
`webchat disconnected code=${code} reason=${logReason || "n/a"} conn=${connId}`,
);
}
if (client?.presenceKey) {
@@ -208,7 +236,7 @@ export function attachGatewayWsConnectionHandler(params: {
logWs("out", "close", {
connId,
code,
reason: reason?.toString(),
reason: logReason,
durationMs,
cause: closeCause,
handshake: handshakeState,