fix(exec): escape invisible approval filler chars

This commit is contained in:
Peter Steinberger
2026-03-22 22:51:00 -07:00
parent 78175aeb0a
commit 4d50084c6e
6 changed files with 54 additions and 3 deletions

View File

@@ -109,6 +109,7 @@ Docs: https://docs.openclaw.ai
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) Thanks @scoootscooob.
- Mattermost/threading: honor `replyToMode: "off"` for already-threaded inbound posts so threaded follow-ups can fall back to top-level replies when configured. (#52543) Thanks @RichardCao.
- Security/exec approvals: escape blank Hangul filler code points in approval prompts across gateway/chat and the macOS native approval UI so visually empty Unicode padding cannot hide reviewed command text.
- Telegram/replies: set `allow_sending_without_reply` on reply-targeted sends and media-error notices so deleted parent messages no longer drop otherwise valid replies. (#52524) Thanks @moltbot886.
- Gateway/status: resolve env-backed `gateway.auth.*` SecretRefs before read-only probe auth checks so status no longer reports false probe failures when auth is configured through SecretRef. (#52513) Thanks @CodeForgeNet.
- Agents/exec: return plain-text failed tool output for timeouts and other non-success exec outcomes so models no longer parrot raw JSON error payloads back to users. (#52508) Thanks @martingarramon.

View File

@@ -0,0 +1,31 @@
import Foundation
enum ExecApprovalCommandDisplaySanitizer {
private static let invisibleCodePoints: Set<UInt32> = [
0x115F,
0x1160,
0x3164,
0xFFA0,
]
static func sanitize(_ text: String) -> String {
var sanitized = ""
sanitized.reserveCapacity(text.count)
for scalar in text.unicodeScalars {
if self.shouldEscape(scalar) {
sanitized.append(self.escape(scalar))
} else {
sanitized.append(String(scalar))
}
}
return sanitized
}
private static func shouldEscape(_ scalar: UnicodeScalar) -> Bool {
scalar.properties.generalCategory == .format || self.invisibleCodePoints.contains(scalar.value)
}
private static func escape(_ scalar: UnicodeScalar) -> String {
"\\u{\(String(scalar.value, radix: 16, uppercase: true))}"
}
}

View File

@@ -271,7 +271,7 @@ enum ExecApprovalsPromptPresenter {
commandText.drawsBackground = true
commandText.backgroundColor = NSColor.textBackgroundColor
commandText.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
commandText.string = request.command
commandText.string = ExecApprovalCommandDisplaySanitizer.sanitize(request.command)
commandText.textContainerInset = NSSize(width: 6, height: 6)
commandText.textContainer?.lineFragmentPadding = 0
commandText.textContainer?.widthTracksTextView = true

View File

@@ -0,0 +1,12 @@
import Foundation
import Testing
@testable import OpenClaw
struct ExecApprovalCommandDisplaySanitizerTests {
@Test func `escapes invisible command spoofing characters`() {
let input = "date\u{200B}\u{3164}\u{FFA0}\u{115F}\u{1160}가"
#expect(
ExecApprovalCommandDisplaySanitizer.sanitize(input) ==
"date\\u{200B}\\u{3164}\\u{FFA0}\\u{115F}\\u{1160}가")
}
}

View File

@@ -8,6 +8,12 @@ describe("sanitizeExecApprovalDisplayText", () => {
it("escapes unicode format characters but leaves other text intact", () => {
expect(sanitizeExecApprovalDisplayText("echo hi\u200Bthere")).toBe("echo hi\\u{200B}there");
});
it("escapes visually blank hangul filler characters used for spoofing", () => {
expect(sanitizeExecApprovalDisplayText("date\u3164\uFFA0\u115F\u1160가")).toBe(
"date\\u{3164}\\u{FFA0}\\u{115F}\\u{1160}가",
);
});
});
describe("resolveExecApprovalCommandDisplay", () => {

View File

@@ -1,13 +1,14 @@
import type { ExecApprovalRequestPayload } from "./exec-approvals.js";
const UNICODE_FORMAT_CHAR_REGEX = /\p{Cf}/gu;
// Escape invisible characters that can spoof approval prompts in common UIs.
const EXEC_APPROVAL_INVISIBLE_CHAR_REGEX = /[\p{Cf}\u115F\u1160\u3164\uFFA0]/gu;
function formatCodePointEscape(char: string): string {
return `\\u{${char.codePointAt(0)?.toString(16).toUpperCase() ?? "FFFD"}}`;
}
export function sanitizeExecApprovalDisplayText(commandText: string): string {
return commandText.replace(UNICODE_FORMAT_CHAR_REGEX, formatCodePointEscape);
return commandText.replace(EXEC_APPROVAL_INVISIBLE_CHAR_REGEX, formatCodePointEscape);
}
function normalizePreview(commandText: string, commandPreview?: string | null): string | null {