diff --git a/backend/app/api/v1/config_repos.py b/backend/app/api/v1/config_repos.py index 0ca58f9a..032ac645 100644 --- a/backend/app/api/v1/config_repos.py +++ b/backend/app/api/v1/config_repos.py @@ -86,6 +86,14 @@ def _auth_ref_exists(auth_ref: str) -> bool: return False override = os.environ.get("RELYLOOP_SECRETS_DIR") secrets_root = Path(override).resolve() if override else Path("./secrets").resolve() + # Path containment guard. `auth_ref` is already constrained to + # `^[a-zA-Z0-9_-]+$` by CreateConfigRepoRequest (no slashes/dots → no + # traversal at the API boundary); this resolve()+relative_to() is a second + # layer that also catches symlink escape and any non-HTTP caller. CodeQL's + # py/path-injection flags this candidate/is_file() pair because it doesn't + # model the Pydantic pattern or recognize relative_to() as a sanitizer — + # dismissed as a reviewed false positive (the input cannot escape + # secrets_root). candidate = (secrets_root / auth_ref).resolve() try: candidate.relative_to(secrets_root) diff --git a/ui/scripts/gen-types.mjs b/ui/scripts/gen-types.mjs index 68b673ac..2f16d1f0 100644 --- a/ui/scripts/gen-types.mjs +++ b/ui/scripts/gen-types.mjs @@ -13,7 +13,7 @@ * Or via the package script: pnpm types:gen */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; @@ -40,7 +40,14 @@ const BANNER = `// SPDX-FileCopyrightText: 2026 soundminds.ai `; console.log(`Generating ${OUTPUT} from ${SOURCE_URL}…`); -execSync(`npx openapi-typescript ${SOURCE_URL} -o ${OUTPUT}`, { +// execFileSync (no shell) instead of execSync with an interpolated string: +// SOURCE_URL comes from the OPENAPI_URL env var, and an interpolated shell +// command would let a crafted value inject. Passing args as an array runs the +// binary directly with no shell, so there is nothing to inject into. On Windows +// the launcher is `npx.cmd` (no shell to resolve the `.cmd` extension), so pick +// the platform-correct executable name. +const NPX = process.platform === 'win32' ? 'npx.cmd' : 'npx'; +execFileSync(NPX, ['openapi-typescript', SOURCE_URL, '-o', OUTPUT], { stdio: 'inherit', cwd: UI_ROOT, });