mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 15:31:23 +00:00
fix(inbound): preserve literal backslash-n sequences in Windows paths (#11547)
* fix(inbound): preserve literal backslash-n sequences in Windows paths
The normalizeInboundTextNewlines function was converting literal backslash-n
sequences (\n) to actual newlines, corrupting Windows paths like
C:\Work\nxxx\README.md when sent through WebUI.
This fix removes the .replaceAll("\\n", "\n") operation, preserving
literal backslash-n sequences while still normalizing actual CRLF/CR to LF.
Fixes #7968
* fix(test): set RawBody to Windows path so BodyForAgent fallback chain tests correctly
* fix: tighten Windows path newline regression coverage (#11547) (thanks @mcaxtr)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- 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.
|
- 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.
|
||||||
|
- Inbound/Web UI: preserve literal `\n` sequences when normalizing inbound text so Windows paths like `C:\\Work\\nxxx\\README.md` are not corrupted. (#11547) Thanks @mcaxtr.
|
||||||
- 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/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: 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/Gateway: sanitize and truncate untrusted WebSocket header values in pre-handshake close logs to reduce log-poisoning risk. Thanks @thewilloftheshadow.
|
||||||
|
|||||||
@@ -61,16 +61,19 @@ describe("normalizeInboundTextNewlines", () => {
|
|||||||
expect(normalizeInboundTextNewlines("a\rb")).toBe("a\nb");
|
expect(normalizeInboundTextNewlines("a\rb")).toBe("a\nb");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("decodes literal \\n to newlines when no real newlines exist", () => {
|
it("preserves literal backslash-n sequences (Windows paths)", () => {
|
||||||
expect(normalizeInboundTextNewlines("a\\nb")).toBe("a\nb");
|
// Windows paths like C:\Work\nxxx should NOT have \n converted to newlines
|
||||||
|
expect(normalizeInboundTextNewlines("a\\nb")).toBe("a\\nb");
|
||||||
|
expect(normalizeInboundTextNewlines("C:\\Work\\nxxx")).toBe("C:\\Work\\nxxx");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("finalizeInboundContext", () => {
|
describe("finalizeInboundContext", () => {
|
||||||
it("fills BodyForAgent/BodyForCommands and normalizes newlines", () => {
|
it("fills BodyForAgent/BodyForCommands and normalizes newlines", () => {
|
||||||
const ctx: MsgContext = {
|
const ctx: MsgContext = {
|
||||||
Body: "a\\nb\r\nc",
|
// Use actual CRLF for newline normalization test, not literal \n sequences
|
||||||
RawBody: "raw\\nline",
|
Body: "a\r\nb\r\nc",
|
||||||
|
RawBody: "raw\r\nline",
|
||||||
ChatType: "channel",
|
ChatType: "channel",
|
||||||
From: "whatsapp:group:123@g.us",
|
From: "whatsapp:group:123@g.us",
|
||||||
GroupSubject: "Test",
|
GroupSubject: "Test",
|
||||||
@@ -87,6 +90,20 @@ describe("finalizeInboundContext", () => {
|
|||||||
expect(out.ConversationLabel).toContain("Test");
|
expect(out.ConversationLabel).toContain("Test");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves literal backslash-n in Windows paths", () => {
|
||||||
|
const ctx: MsgContext = {
|
||||||
|
Body: "C:\\Work\\nxxx\\README.md",
|
||||||
|
RawBody: "C:\\Work\\nxxx\\README.md",
|
||||||
|
ChatType: "direct",
|
||||||
|
From: "web:user",
|
||||||
|
};
|
||||||
|
|
||||||
|
const out = finalizeInboundContext(ctx);
|
||||||
|
expect(out.Body).toBe("C:\\Work\\nxxx\\README.md");
|
||||||
|
expect(out.BodyForAgent).toBe("C:\\Work\\nxxx\\README.md");
|
||||||
|
expect(out.BodyForCommands).toBe("C:\\Work\\nxxx\\README.md");
|
||||||
|
});
|
||||||
|
|
||||||
it("can force BodyForCommands to follow updated CommandBody", () => {
|
it("can force BodyForCommands to follow updated CommandBody", () => {
|
||||||
const ctx: MsgContext = {
|
const ctx: MsgContext = {
|
||||||
Body: "base",
|
Body: "base",
|
||||||
|
|||||||
35
src/auto-reply/reply/inbound-text.test.ts
Normal file
35
src/auto-reply/reply/inbound-text.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { normalizeInboundTextNewlines } from "./inbound-text.js";
|
||||||
|
|
||||||
|
describe("normalizeInboundTextNewlines", () => {
|
||||||
|
it("converts CRLF to LF", () => {
|
||||||
|
expect(normalizeInboundTextNewlines("hello\r\nworld")).toBe("hello\nworld");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts CR to LF", () => {
|
||||||
|
expect(normalizeInboundTextNewlines("hello\rworld")).toBe("hello\nworld");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves literal backslash-n sequences in Windows paths", () => {
|
||||||
|
// Windows paths like C:\Work\nxxx should NOT have \n converted to newlines
|
||||||
|
const windowsPath = "C:\\Work\\nxxx\\README.md";
|
||||||
|
expect(normalizeInboundTextNewlines(windowsPath)).toBe("C:\\Work\\nxxx\\README.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves backslash-n in messages containing Windows paths", () => {
|
||||||
|
const message = "Please read the file at C:\\Work\\nxxx\\README.md";
|
||||||
|
expect(normalizeInboundTextNewlines(message)).toBe(
|
||||||
|
"Please read the file at C:\\Work\\nxxx\\README.md",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves multiple backslash-n sequences", () => {
|
||||||
|
const message = "C:\\new\\notes\\nested";
|
||||||
|
expect(normalizeInboundTextNewlines(message)).toBe("C:\\new\\notes\\nested");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still normalizes actual CRLF while preserving backslash-n", () => {
|
||||||
|
const message = "Line 1\r\nC:\\Work\\nxxx";
|
||||||
|
expect(normalizeInboundTextNewlines(message)).toBe("Line 1\nC:\\Work\\nxxx");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
export function normalizeInboundTextNewlines(input: string): string {
|
export function normalizeInboundTextNewlines(input: string): string {
|
||||||
return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("\\n", "\n");
|
// Normalize actual newline characters (CR+LF and CR to LF).
|
||||||
|
// Do NOT replace literal backslash-n sequences (\\n) as they may be part of
|
||||||
|
// Windows paths like C:\Work\nxxx\README.md or user-intended escape sequences.
|
||||||
|
return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user