From b8cae0caf6f0596c9b9c1af1be4efc008de47e13 Mon Sep 17 00:00:00 2001 From: carole-lavillonniere Date: Mon, 12 Oct 2026 11:56:11 +0200 Subject: [PATCH] fix(extensions): drop the git URL source parameter --- src/core/analytics.ts | 2 +- src/lib/localstack/extensions.logic.test.ts | 56 ++------------------- src/lib/localstack/extensions.logic.ts | 24 +-------- src/tools/localstack-extensions.ts | 32 ++++-------- 4 files changed, 15 insertions(+), 99 deletions(-) diff --git a/src/core/analytics.ts b/src/core/analytics.ts index 522b50e..f57c0f5 100644 --- a/src/core/analytics.ts +++ b/src/core/analytics.ts @@ -62,7 +62,7 @@ export const TOOL_ARG_ALLOWLIST: Record = { "cloudPod", "envVarKeys", ], - "localstack-extensions": ["action", "name", "source"], + "localstack-extensions": ["action", "name"], "localstack-iam-policy-analyzer": ["action", "mode"], "localstack-logs-analysis": ["analysisType", "lines", "service", "operation", "filter"], "localstack-management": ["action", "service", "envVars"], diff --git a/src/lib/localstack/extensions.logic.test.ts b/src/lib/localstack/extensions.logic.test.ts index bb61f72..e5804ef 100644 --- a/src/lib/localstack/extensions.logic.test.ts +++ b/src/lib/localstack/extensions.logic.test.ts @@ -162,68 +162,18 @@ describe("validateExtensionTarget", () => { } }); - test("accepts documented git+https sources on known hosts", () => { + test("rejects git URLs, which LocalStack no longer installs", () => { expect( validateExtensionTarget({ - source: - "git+https://github.com/localstack/localstack-extensions.git#egg=localstack-keycloak&subdirectory=keycloak", + name: "git+https://github.com/localstack/localstack-extensions.git#egg=localstack-keycloak&subdirectory=keycloak", }) - ).toBeNull(); - expect( - validateExtensionTarget({ source: "git+https://gitlab.com/org/repo.git@v1.2.3" }) - ).toBeNull(); - }); - - test("rejects non-https, unknown-host, and non-git sources", () => { - for (const source of [ - "git+ssh://git@github.com/org/repo.git", - "git+https://evil.example/org/repo.git", - "https://github.com/org/repo.git", - "file:///etc/passwd", - "git+https://github.com/org/repo.git --index-url=x", - ]) { - expect(validateExtensionTarget({ source })).toMatch(/not a supported extension source/); - } - }); - - test("rejects host-confusion sources (suffix / userinfo tricks)", () => { - for (const source of [ - "git+https://github.com.evil.com/org/repo.git", - "git+https://github.com@evil.com/org/repo.git", - "git+https://evil.com@github.com/org/repo.git", - "git+https://github.comX/org/repo.git", - ]) { - expect(validateExtensionTarget({ source })).toMatch(/not a supported extension source/); - } - }); - - test("rejects '..' path traversal in the source fragment (subdirectory=)", () => { - expect( - validateExtensionTarget({ - source: "git+https://github.com/org/repo#subdirectory=../../../../opt/code", - }) - ).toMatch(/'\.\.'/); - expect( - validateExtensionTarget({ - source: "git+https://github.com/org/repo#egg=x&subdirectory=../../etc", - }) - ).toMatch(/'\.\.'/); - }); - - test("treats an empty name as absent so a source-only install is accepted", () => { - expect( - validateExtensionTarget({ - name: "", - source: "git+https://github.com/org/repo.git", - }) - ).toBeNull(); + ).toMatch(/not a valid extension package name/); }); test("validates in linear time (no catastrophic backtracking)", () => { const evil = "a".repeat(100000); const start = Date.now(); validateExtensionTarget({ name: `${evil} ` }); // fails the regex - validateExtensionTarget({ source: `git+https://github.com/${evil}` }); expect(Date.now() - start).toBeLessThan(1000); }); }); diff --git a/src/lib/localstack/extensions.logic.ts b/src/lib/localstack/extensions.logic.ts index f7ab304..acd3844 100644 --- a/src/lib/localstack/extensions.logic.ts +++ b/src/lib/localstack/extensions.logic.ts @@ -43,33 +43,11 @@ export const EXTENSIONS_VENV_REPAIR_SCRIPT = [ export const EXTENSION_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*(\[[A-Za-z0-9,._-]+\])?(==[A-Za-z0-9.*+!-]+)?$/; -/** Git sources are limited to https on well-known hosts (matches the documented usage). */ -export const EXTENSION_GIT_SOURCE_RE = - /^git\+https:\/\/(github\.com|gitlab\.com|bitbucket\.org)\/[A-Za-z0-9._/-]+(@[A-Za-z0-9._/-]+)?(#[A-Za-z0-9._&=/-]+)?$/; - /** Returns an error message for an invalid install/uninstall target, or null when fine. */ -export function validateExtensionTarget({ - name, - source, -}: { - name?: string; - source?: string; -}): string | null { - // Treat an empty name as "not provided" so a source-only install isn't rejected - // for a name it never supplied. +export function validateExtensionTarget({ name }: { name?: string }): string | null { if (name && !EXTENSION_NAME_RE.test(name)) { return `"${name}" is not a valid extension package name. Expected a PyPI-style name like localstack-extension-typedb, optionally with extras or a ==version pin.`; } - if (source) { - if (!EXTENSION_GIT_SOURCE_RE.test(source)) { - return `"${source}" is not a supported extension source. Use a git+https:// URL on github.com, gitlab.com, or bitbucket.org (e.g. git+https://github.com/org/repo.git#egg=my-extension).`; - } - // Defense-in-depth: reject `..` path segments in the fragment (pip's subdirectory= - // could otherwise point its build root outside the clone). - if (/(^|[?#&/])\.\.(?=$|[/&])/.test(source)) { - return `"${source}" is not a supported extension source: '..' path segments are not allowed.`; - } - } return null; } diff --git a/src/tools/localstack-extensions.ts b/src/tools/localstack-extensions.ts index 2e1bbce..482d563 100644 --- a/src/tools/localstack-extensions.ts +++ b/src/tools/localstack-extensions.ts @@ -42,12 +42,6 @@ export const schema = { .describe( "Extension package name (e.g. 'localstack-extension-typedb' or 'localstack-extension-typedb==1.0.0'). Required for install and uninstall actions." ), - source: z - .string() - .optional() - .describe( - "Git URL to install from (e.g. 'git+https://github.com/org/repo.git'). Use this instead of name when installing from a repository." - ), }; export const metadata: ToolMetadata = { @@ -61,12 +55,8 @@ export const metadata: ToolMetadata = { }, }; -export default async function localstackExtensions({ - action, - name, - source, -}: InferSchema) { - return withToolAnalytics("localstack-extensions", { action, name, source }, async () => { +export default async function localstackExtensions({ action, name }: InferSchema) { + return withToolAnalytics("localstack-extensions", { action, name }, async () => { // `available` only needs the platform API; the container-touching actions need a // running LocalStack (the extension manager runs inside it) + the Docker daemon. const checks = @@ -86,7 +76,7 @@ export default async function localstackExtensions({ case "list": return await handleList(); case "install": - return await handleInstall(name, source); + return await handleInstall(name); case "uninstall": return await handleUninstall(name); case "available": @@ -183,22 +173,20 @@ async function handleList() { return ResponseBuilder.markdown(formatInstalledExtensions(extensions)); } -async function handleInstall(name?: string, source?: string) { - const hasName = !!name; - const hasSource = !!source; - if ((hasName && hasSource) || (!hasName && !hasSource)) { +async function handleInstall(name?: string) { + if (!name) { return ResponseBuilder.error( - "Invalid Parameters", - "Provide either `name` or `source` for install, but not both." + "Missing Required Parameter", + "The `install` action requires the `name` parameter to be specified." ); } - const validationError = validateExtensionTarget({ name, source }); + const validationError = validateExtensionTarget({ name }); if (validationError) { return ResponseBuilder.error("Invalid Extension Target", validationError); } - const target = source || name!; + const target = name; let result; try { result = await runExtensionManager(["install", target], { repairVenv: true }); @@ -216,7 +204,7 @@ async function handleInstall(name?: string, source?: string) { if (outcome.kind === "not-found") { return ResponseBuilder.error( "Extension Not Found", - `Could not resolve the extension package '${target}'. Please verify it exists on PyPI, or provide a git repository URL using the source parameter.` + `Could not resolve the extension package '${target}'. Please verify it exists on PyPI.` ); } if (!outcome.success) {