mirror of https://github.com/openclaw/openclaw.git
fix(exec): escape invisible approval filler chars
This commit is contained in:
parent
78175aeb0a
commit
4d50084c6e
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}가")
|
||||
}
|
||||
}
|
||||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue