From 368bc7de549b8bb1581184aa3decc62bee7b0f71 Mon Sep 17 00:00:00 2001 From: enesgules Date: Mon, 7 Sep 2026 15:48:56 +0300 Subject: [PATCH] fix(cli): strip terminal control characters from API content Library titles, descriptions, and doc snippets are crowdsourced. Sanitize them at the API boundary so a malicious entry cannot inject ANSI/OSC escape sequences into the user's terminal via ctx7 docs or ctx7 library. Reported by Syed Anas Mohiuddin. Claude-Session: https://claude.ai/code/session_01XXTv4m4tzcTPQYfa9DvGsv --- .changeset/strip-control-chars-cli.md | 5 ++++ .../src/__tests__/strip-control-chars.test.ts | 13 ++++++++++ packages/cli/src/utils/api.ts | 24 +++++++++++++++---- 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 .changeset/strip-control-chars-cli.md create mode 100644 packages/cli/src/__tests__/strip-control-chars.test.ts diff --git a/.changeset/strip-control-chars-cli.md b/.changeset/strip-control-chars-cli.md new file mode 100644 index 000000000..ecee10d42 --- /dev/null +++ b/.changeset/strip-control-chars-cli.md @@ -0,0 +1,5 @@ +--- +"ctx7": patch +--- + +Strip terminal control characters from crowdsourced API content (library titles, descriptions, docs) before printing, preventing ANSI/OSC escape sequence injection in `ctx7 docs` and `ctx7 library` output. Reported by Syed Anas Mohiuddin. diff --git a/packages/cli/src/__tests__/strip-control-chars.test.ts b/packages/cli/src/__tests__/strip-control-chars.test.ts new file mode 100644 index 000000000..6c954e9bf --- /dev/null +++ b/packages/cli/src/__tests__/strip-control-chars.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { stripControlChars } from "../utils/api.js"; + +describe("stripControlChars", () => { + it("removes escape sequences from nested API content, keeps newlines and tabs", () => { + const evil = "\x1b]52;c;ZWNobyBwd25k\x07\x1b[2J\x1b[Htitle\r\n\tok\x9b1m"; + expect(stripControlChars(evil)).toBe("]52;c;ZWNobyBwd25k[2J[Htitle\n\tok1m"); + expect(stripControlChars({ a: [evil, 1, null], b: { c: evil } })).toEqual({ + a: ["]52;c;ZWNobyBwd25k[2J[Htitle\n\tok1m", 1, null], + b: { c: "]52;c;ZWNobyBwd25k[2J[Htitle\n\tok1m" }, + }); + }); +}); diff --git a/packages/cli/src/utils/api.ts b/packages/cli/src/utils/api.ts index 50b039b57..f7d2c1a98 100644 --- a/packages/cli/src/utils/api.ts +++ b/packages/cli/src/utils/api.ts @@ -16,6 +16,22 @@ import { VERSION } from "../constants.js"; let baseUrl = "https://context7.com"; +// Library metadata and docs are crowdsourced. Strip terminal control characters +// (C0 except \t\n, DEL, C1) so a malicious entry cannot inject escape sequences. + +const CONTROL_CHARS = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g; + +export function stripControlChars(value: T): T { + if (typeof value === "string") return value.replace(CONTROL_CHARS, "") as T; + if (Array.isArray(value)) return value.map(stripControlChars) as T; + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, stripControlChars(v)]) + ) as T; + } + return value; +} + export function getBaseUrl(): string { return baseUrl; } @@ -113,7 +129,7 @@ export async function searchLibraries( headers["Authorization"] = `Bearer ${accessToken}`; } const response = await fetch(`${baseUrl}/api/v2/libs/search?${params}`, { headers }); - return (await response.json()) as LibrarySearchResponse; + return stripControlChars((await response.json()) as LibrarySearchResponse); } export async function getSkillQuota(accessToken: string): Promise { @@ -306,7 +322,7 @@ export async function resolveLibrary( }; } - return (await response.json()) as LibrarySearchResponse; + return stripControlChars((await response.json()) as LibrarySearchResponse); } export interface GetContextOptions { @@ -354,8 +370,8 @@ export async function getLibraryContext( } if (options?.type === "txt") { - return await response.text(); + return stripControlChars(await response.text()); } - return (await response.json()) as ContextResponse; + return stripControlChars((await response.json()) as ContextResponse); }