fix(security): harden hook session-key normalization (#25750) (thanks @bmendonca3)

This commit is contained in:
Peter Steinberger
2026-02-24 23:47:59 +00:00
parent 006d0a2430
commit a327952d65
3 changed files with 19 additions and 2 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security/Hooks: normalize hook session-key classification with trim/lowercase plus Unicode NFKC folding (for example full-width `...`) so external-content wrapping cannot be bypassed by mixed-case or lookalike prefixes. (#25750) Thanks @bmendonca3.
- Discord/Block streaming: restore block-streamed reply delivery by suppressing only reasoning payloads (instead of all `block` payloads), fixing missing Discord replies in `channels.discord.streaming=block` mode. (#25839, #25836, #25792) Thanks @pewallin.
- Matrix/Read receipts: send read receipts as soon as Matrix messages arrive (before handler pipeline work), so clients no longer show long-lived unread/sent states while replies are processing. (#25841, #25840) Thanks @joshjhall.
- Sandbox/FS bridge: build canonical-path shell scripts with newline separators (not `; ` joins) to avoid POSIX `sh` `do;` syntax errors that broke sandbox file/image read-write operations. (#25737, #25824, #25868) Thanks @DennisGoldfinger and @peteragility.

View File

@@ -252,6 +252,11 @@ describe("external-content security", () => {
expect(isExternalHookSession(" HOOK:webhook:123 ")).toBe(true);
});
it("identifies Unicode full-width hook prefixes", () => {
expect(isExternalHookSession(":gmail:msg-123")).toBe(true);
expect(isExternalHookSession("custom:456")).toBe(true);
});
it("rejects non-hook sessions", () => {
expect(isExternalHookSession("cron:daily-task")).toBe(false);
expect(isExternalHookSession("agent:main")).toBe(false);
@@ -278,6 +283,11 @@ describe("external-content security", () => {
expect(getHookType("Hook:custom:456")).toBe("webhook");
});
it("returns hook type for Unicode full-width hook prefixes", () => {
expect(getHookType(":gmail:msg-123")).toBe("email");
expect(getHookType(" custom:456 ")).toBe("webhook");
});
it("returns unknown for non-hook sessions", () => {
expect(getHookType("cron:daily")).toBe("unknown");
});

View File

@@ -285,8 +285,14 @@ export function buildSafeExternalPrompt(params: {
/**
* Checks if a session key indicates an external hook source.
*/
function normalizeHookSessionKey(sessionKey: string): string {
// NFKC folds common full-width forms so hook-prefix checks cannot be bypassed by
// visually similar Unicode spellings like "...".
return sessionKey.normalize("NFKC").trim().toLowerCase();
}
export function isExternalHookSession(sessionKey: string): boolean {
const normalized = sessionKey.trim().toLowerCase();
const normalized = normalizeHookSessionKey(sessionKey);
return (
normalized.startsWith("hook:gmail:") ||
normalized.startsWith("hook:webhook:") ||
@@ -298,7 +304,7 @@ export function isExternalHookSession(sessionKey: string): boolean {
* Extracts the hook type from a session key.
*/
export function getHookType(sessionKey: string): ExternalContentSource {
const normalized = sessionKey.trim().toLowerCase();
const normalized = normalizeHookSessionKey(sessionKey);
if (normalized.startsWith("hook:gmail:")) {
return "email";
}