mirror of
https://github.com/upstash/context7.git
synced 2026-09-14 19:09:34 +08:00
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
This commit is contained in:
@@ -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.
|
||||
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<T>(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<SkillQuotaResponse> {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user