Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl
10 changes: 9 additions & 1 deletion open-sse/executors/chatgpt-web/citations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ function asArray(value: unknown): unknown[] {
}

function markdownLinkText(value: string): string {
return value.replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\n/g, " ").trim();
// Escape the backslash first — otherwise a label ending in (or containing) a
// backslash leaks past the `[…]` escaping and breaks the generated link, e.g.
// `[Path C:\](url)` where the trailing `\` escapes the closing bracket.
return value
.replace(/\\/g, "\\\\")
.replace(/\[/g, "\\[")
.replace(/\]/g, "\\]")
.replace(/\n/g, " ")
.trim();
}

function markdownUrl(value: string): string {
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/chatgpt-web-citations-escape.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// ChatGPT-web citation link-text escaping (CodeQL js/incomplete-sanitization,
// PR #6569 release-blocker).
//
// `markdownLinkText()` builds the `[text]` half of a Markdown link from an
// untrusted citation label. It escaped `[` and `]` but NOT the backslash
// itself, so a label ending in (or containing) a backslash produced a broken
// link: e.g. `[Path C:\](url)` — the trailing `\` escapes the closing `]`,
// consuming the link's bracket. The escape character must be escaped first.

import test from "node:test";
import assert from "node:assert/strict";

const { cleanChatGptText } = await import(
"../../open-sse/executors/chatgpt-web/citations.ts"
);

const S = ""; // marker start
const SEP = ""; // marker separator
const E = ""; // marker end

// Build a raw `url` citation marker:  url  <label>  <url> 
const urlMarker = (label: string, url: string) => `${S}url${SEP}${label}${SEP}${url}${E}`;

test("markdownLinkText escapes a trailing backslash in the citation label", () => {
// Label ends in a backslash — without escaping, `[Path C:\](url)` breaks the link.
const text = `See ${urlMarker("Path C:\\", "https://example.com/docs")} here`;
const out = cleanChatGptText(text);
assert.equal(out, "See [Path C:\\\\](https://example.com/docs) here");
// The backslash must be doubled (escaped), never left bare before the `]`.
assert.doesNotMatch(out, /[^\\]\\\]/);
});

test("markdownLinkText escapes a backslash preceding a bracket (no bracket leak)", () => {
const text = urlMarker("a\\[b", "https://example.com");
const out = cleanChatGptText(text);
// `\` → `\\`, then `[` → `\[` : label renders as literal `a\[b`.
assert.equal(out, "[a\\\\\\[b](https://example.com)");
});

test("bracket-only labels keep their existing escaping (regression guard)", () => {
const text = urlMarker("a[b]c", "https://example.com");
const out = cleanChatGptText(text);
assert.equal(out, "[a\\[b\\]c](https://example.com)");
});

test("plain labels with no metacharacters pass through unchanged", () => {
const text = urlMarker("Plain Title", "https://example.com");
const out = cleanChatGptText(text);
assert.equal(out, "[Plain Title](https://example.com)");
});