diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts index 9404e703ca6..4cd749ba913 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import { describe, expect, it } from "vitest"; import { buildCurlCommand, curlPayload } from "./curlCommand"; @@ -12,7 +13,7 @@ describe("buildCurlCommand", () => { const command = buildCurlCommand(input); expect(command).toContain("curl -X POST 'http://localhost:4000/v1/systemone'"); - expect(command).toContain("-H 'Authorization: Bearer $LITELLM_API_KEY'"); + expect(command).toContain('-H "Authorization: Bearer $LITELLM_API_KEY"'); expect(command).not.toContain("sk-"); expect(command).toContain(' "state": "it\'\\\'\'s broken"'); expect(JSON.parse(curlPayload(command) ?? "")).toEqual({ model: "jev-latest", state: "it's broken" }); @@ -37,10 +38,26 @@ describe("buildCurlCommand", () => { authHeaderName: "X-Litellm-Key", }; const command = buildCurlCommand(input); - expect(command).toContain("-H 'X-Litellm-Key: Bearer $LITELLM_API_KEY'"); + expect(command).toContain('-H "X-Litellm-Key: Bearer $LITELLM_API_KEY"'); expect(command).not.toContain("Authorization"); }); + it("lets the shell expand the key while a hostile header name stays literal", () => { + const input = { + baseUrl: "https://proxy.example", + endpoint: "/v1/systemone" as const, + payload: "{}", + authHeaderName: 'X-"$(touch pwned)`id`\\Key', + }; + const command = buildCurlCommand(input); + const args = execFileSync("bash", ["-c", command.replace(/^curl /, "printf '%s\\n' ")], { + encoding: "utf8", + env: { ...process.env, LITELLM_API_KEY: "sk-test-key" }, + }); + expect(args).toContain('X-"$(touch pwned)`id`\\Key: Bearer sk-test-key\n'); + expect(args).not.toContain("$LITELLM_API_KEY"); + }); + it("passes a payload that is not JSON through untouched", () => { const input = { baseUrl: "https://proxy.example", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts index 1153107cade..27d44bb0b04 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/curlCommand.ts @@ -9,6 +9,8 @@ export const CURL_KEY_PLACEHOLDER = "$LITELLM_API_KEY"; const shellSingleQuote = (value: string): string => `'${value.replace(/'/g, `'\\''`)}'`; +const shellDoubleQuoteLiteral = (value: string): string => value.replace(/["\\$`]/g, (char) => `\\${char}`); + const prettyPayload = (payload: string): string => { try { return JSON.stringify(JSON.parse(payload), null, 2); @@ -20,7 +22,7 @@ const prettyPayload = (payload: string): string => { export const buildCurlCommand = ({ baseUrl, endpoint, payload, authHeaderName }: CurlCommandInput): string => [ `curl -X POST ${shellSingleQuote(`${baseUrl.replace(/\/+$/, "")}${endpoint}`)}`, - ` -H ${shellSingleQuote(`${authHeaderName}: Bearer ${CURL_KEY_PLACEHOLDER}`)}`, + ` -H "${shellDoubleQuoteLiteral(authHeaderName)}: Bearer ${CURL_KEY_PLACEHOLDER}"`, ` -H "Content-Type: application/json"`, ` -d ${shellSingleQuote(prettyPayload(payload))}`, ].join(" \\\n");