mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
fix(security): remediate CodeQL alerts
This commit is contained in:
@@ -63,6 +63,15 @@ describe("sanitizeForPlainText", () => {
|
||||
expect(sanitizeForPlainText('<a href="https://example.com">link</a>')).toBe("link");
|
||||
});
|
||||
|
||||
it("keeps stripping tags exposed by malformed tag text", () => {
|
||||
const sanitized = sanitizeForPlainText(
|
||||
"before <<script>script>alert(1)</<script>script> after",
|
||||
);
|
||||
|
||||
expect(sanitized).toBe("before alert(1) after");
|
||||
expect(sanitized).not.toContain("<script");
|
||||
});
|
||||
|
||||
it("strips known internal runtime scaffolding tags including underscore names", () => {
|
||||
expect(sanitizeForPlainText("ok <previous_response>null</previous_response> done")).toBe(
|
||||
"ok done",
|
||||
|
||||
@@ -25,6 +25,17 @@ const INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE = new RegExp(
|
||||
`<\\s*\\/?\\s*(?:${INTERNAL_RUNTIME_SCAFFOLDING_TAG_PATTERN})\\b[^>]*>`,
|
||||
"gi",
|
||||
);
|
||||
const HTML_TAG_RE = /<\/?[a-z][a-z0-9_-]*\b[^>]*>/gi;
|
||||
|
||||
function stripRemainingHtmlTags(text: string): string {
|
||||
let previous: string;
|
||||
let current = text;
|
||||
do {
|
||||
previous = current;
|
||||
current = current.replace(HTML_TAG_RE, "");
|
||||
} while (current !== previous);
|
||||
return current;
|
||||
}
|
||||
|
||||
export function stripInternalRuntimeScaffolding(text: string): string {
|
||||
return text
|
||||
@@ -42,29 +53,25 @@ export function stripInternalRuntimeScaffolding(text: string): string {
|
||||
* prose (e.g. `a < b`).
|
||||
*/
|
||||
export function sanitizeForPlainText(text: string): string {
|
||||
return (
|
||||
stripInternalRuntimeScaffolding(text)
|
||||
// Preserve angle-bracket autolinks as plain URLs before tag stripping.
|
||||
.replace(/<((?:https?:\/\/|mailto:)[^<>\s]+)>/gi, "$1")
|
||||
// Line breaks
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
// Block elements → newlines
|
||||
.replace(/<\/?(p|div)>/gi, "\n")
|
||||
// Bold → WhatsApp/Signal bold
|
||||
.replace(/<(b|strong)>(.*?)<\/\1>/gi, "*$2*")
|
||||
// Italic → WhatsApp/Signal italic
|
||||
.replace(/<(i|em)>(.*?)<\/\1>/gi, "_$2_")
|
||||
// Strikethrough → WhatsApp/Signal strikethrough
|
||||
.replace(/<(s|strike|del)>(.*?)<\/\1>/gi, "~$2~")
|
||||
// Inline code
|
||||
.replace(/<code>(.*?)<\/code>/gi, "`$1`")
|
||||
// Headings → bold text with newline
|
||||
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n*$1*\n")
|
||||
// List items → bullet points
|
||||
.replace(/<li[^>]*>(.*?)<\/li>/gi, "• $1\n")
|
||||
// Strip remaining HTML tags (require tag-like structure: <word...>)
|
||||
.replace(/<\/?[a-z][a-z0-9_-]*\b[^>]*>/gi, "")
|
||||
// Collapse 3+ consecutive newlines into 2
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
);
|
||||
const converted = stripInternalRuntimeScaffolding(text)
|
||||
// Preserve angle-bracket autolinks as plain URLs before tag stripping.
|
||||
.replace(/<((?:https?:\/\/|mailto:)[^<>\s]+)>/gi, "$1")
|
||||
// Line breaks
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
// Block elements → newlines
|
||||
.replace(/<\/?(p|div)>/gi, "\n")
|
||||
// Bold → WhatsApp/Signal bold
|
||||
.replace(/<(b|strong)>(.*?)<\/\1>/gi, "*$2*")
|
||||
// Italic → WhatsApp/Signal italic
|
||||
.replace(/<(i|em)>(.*?)<\/\1>/gi, "_$2_")
|
||||
// Strikethrough → WhatsApp/Signal strikethrough
|
||||
.replace(/<(s|strike|del)>(.*?)<\/\1>/gi, "~$2~")
|
||||
// Inline code
|
||||
.replace(/<code>(.*?)<\/code>/gi, "`$1`")
|
||||
// Headings → bold text with newline
|
||||
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n*$1*\n")
|
||||
// List items → bullet points
|
||||
.replace(/<li[^>]*>(.*?)<\/li>/gi, "• $1\n");
|
||||
|
||||
return stripRemainingHtmlTags(converted).replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ describe("safeEqualSecret", () => {
|
||||
["secret-token", "secret-token", true],
|
||||
["secret-token", "secret-tokEn", false],
|
||||
["short", "much-longer", false],
|
||||
["", "", true],
|
||||
["", "secret", false],
|
||||
[undefined, "secret", false],
|
||||
["secret", undefined, false],
|
||||
[null, "secret", false],
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { createHash, timingSafeEqual } from "node:crypto";
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
|
||||
function padSecretBytes(bytes: Buffer, length: number): Buffer {
|
||||
if (bytes.length === length) {
|
||||
return bytes;
|
||||
}
|
||||
const padded = Buffer.alloc(length);
|
||||
bytes.copy(padded);
|
||||
return padded;
|
||||
}
|
||||
|
||||
export function safeEqualSecret(
|
||||
provided: string | undefined | null,
|
||||
@@ -7,6 +16,16 @@ export function safeEqualSecret(
|
||||
if (typeof provided !== "string" || typeof expected !== "string") {
|
||||
return false;
|
||||
}
|
||||
const hash = (s: string) => createHash("sha256").update(s).digest();
|
||||
return timingSafeEqual(hash(provided), hash(expected));
|
||||
const providedBytes = Buffer.from(provided, "utf8");
|
||||
const expectedBytes = Buffer.from(expected, "utf8");
|
||||
const byteLength = Math.max(providedBytes.length, expectedBytes.length);
|
||||
if (byteLength === 0) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
timingSafeEqual(
|
||||
padSecretBytes(providedBytes, byteLength),
|
||||
padSecretBytes(expectedBytes, byteLength),
|
||||
) && providedBytes.length === expectedBytes.length
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user