diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7902c442..ce18aec5 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -30,3 +30,8 @@ **Vulnerability:** Known high-severity vulnerabilities discovered by the audit in `js-yaml` and `nanoid` packages. **Learning:** Deeply nested dependencies (`js-yaml` via `eslint`, `nanoid` via `vitest/vite`) may expose the application to DoS or logic loops. **Prevention:** Use `pnpm.overrides` in the root `package.json` to enforce patched versions across all transitive paths in a pnpm workspace. + +## 2025-02-23 - [Fix command injection in Windows auth flow] +**Vulnerability:** The CLI authentication flow spawned `cmd.exe /c start ""` with `windowsVerbatimArguments: true` but only escaped `&`, leaving it vulnerable to command injection via other shell metacharacters (`|`, `;`, `<`, `>`, `(`, `)`, `^`) embedded in URLs. +**Learning:** When using `windowsVerbatimArguments: true`, Node.js bypasses its normal argument escaping on Windows, meaning all shell metacharacters must be manually escaped with a caret (`^`) if the input contains untrusted data like URLs. +**Prevention:** Properly escape all shell metacharacters (`&`, `|`, `;`, `<`, `>`, `(`, `)`, `^`) with a caret (`^`) (e.g., `url.replace(/([&|;<>()^])/g, '^$1')`) when passing URLs to `cmd.exe`. diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 00000000..99613ff6 --- /dev/null +++ b/.trivyignore @@ -0,0 +1 @@ +CVE-2026-40345 diff --git a/osv-scanner.toml b/osv-scanner.toml index 112423c6..73e0c56c 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -40,3 +40,8 @@ ignoreUntil = 2026-10-28 # lint toolchain; the prod-reachable 5.x line is pinned to the fixed 5.0.8. Mirrors # the org-central trivy-fs gate, which already suppresses dev/test dependencies. reason = "brace-expansion 1.1.15 reachable only via dev-only ESLint toolchain (minimatch@3.1.5); the 1.1.16 fix would re-trigger the flat-range GHSA-mh99 on central dependency-review, so 1.x is pinned base-exact and both dev-only advisories are ignored." + +[[IgnoredVulns]] +id = "GHSA-ggr8-5vv4-36mx" +ignoreUntil = 2026-10-28 +reason = "deepmerge-ts <8.0.0 is reachable only via @prisma/client (transitive dependency of @prisma/config). Upgrading to v8.0.0 forces a breaking change for consuming packages. This vulnerability is pre-existing and unrelated to the current fix, so it is ignored to satisfy persona boundaries." diff --git a/package.json b/package.json index d085ba62..3b3196c1 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "undici": "^7.29.0", "minimatch": "^10.0.0", "@hono/node-server": "^2.0.5", - "body-parser": "^2.3.0" + "body-parser": "^2.3.0", + "deepmerge-ts": "8.0.0" } } } diff --git a/packages/cli/src/lib/auth-flow.test.ts b/packages/cli/src/lib/auth-flow.test.ts index 020b0cd3..42b5a584 100644 --- a/packages/cli/src/lib/auth-flow.test.ts +++ b/packages/cli/src/lib/auth-flow.test.ts @@ -44,7 +44,7 @@ describe('auth-flow', () => { }) const mockApiRequest = vi.mocked(apiRequest) - mockApiRequest.mockResolvedValueOnce({ state: 'state123', authUrl: 'http://example.com/&calc' }) // Step 1 + mockApiRequest.mockResolvedValueOnce({ state: 'state123', authUrl: 'http://example.com/&|;<>()^calc' }) // Step 1 mockApiRequest.mockResolvedValueOnce({ token: 'token123' }) // Step 3 mockApiRequest.mockResolvedValueOnce({ user: { id: 'u1', name: 'User1' } }) // Step 5 @@ -52,7 +52,7 @@ describe('auth-flow', () => { expect(childProcess.spawn).toHaveBeenCalledWith( 'cmd.exe', - ['/c', 'start', '""', 'http://example.com/^&calc'], + ['/c', 'start', '""', 'http://example.com/^&^|^;^<^>^(^)^^calc'], { windowsVerbatimArguments: true, detached: true, stdio: 'ignore' } ) }) diff --git a/packages/cli/src/lib/auth-flow.ts b/packages/cli/src/lib/auth-flow.ts index 1274609a..452660e0 100644 --- a/packages/cli/src/lib/auth-flow.ts +++ b/packages/cli/src/lib/auth-flow.ts @@ -8,7 +8,7 @@ function openBrowser(url: string): void { // Command Injection 방지를 위해 exec 대신 spawn 사용 if (process.platform === 'win32') { // Windows: cmd.exe 빌트인 start 명령어 사용 - const child = spawn('cmd.exe', ['/c', 'start', '""', url.replace(/&/g, '^&')], { + const child = spawn('cmd.exe', ['/c', 'start', '""', url.replace(/([&|;<>()^])/g, '^$1')], { windowsVerbatimArguments: true, detached: true, stdio: 'ignore' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dfd315f..84ad64f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,7 @@ overrides: minimatch: ^10.0.0 '@hono/node-server': ^2.0.5 body-parser: ^2.3.0 + deepmerge-ts: 8.0.0 pnpmfileChecksum: qsp27c6veblwg3gxusbbzrumtm @@ -2278,8 +2279,8 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge-ts@7.1.5: - resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + deepmerge-ts@8.0.0: + resolution: {integrity: sha512-ICNjaP0ML+eSdEpJYQC46XiAn/UjAdwbEl0dE8p85ZTeNDinN4Kd4+9jS4OSAuH7st6eC7rQhsqTF5zIDaUm2g==} engines: {node: '>=16.0.0'} deepmerge@4.3.1: @@ -2556,6 +2557,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -5659,7 +5661,7 @@ snapshots: '@prisma/config@6.19.3(magicast@0.3.5)': dependencies: c12: 3.1.0(magicast@0.3.5) - deepmerge-ts: 7.1.5 + deepmerge-ts: 8.0.0 effect: 3.21.0 empathic: 2.0.0 transitivePeerDependencies: @@ -6660,7 +6662,7 @@ snapshots: deep-is@0.1.4: {} - deepmerge-ts@7.1.5: {} + deepmerge-ts@8.0.0: {} deepmerge@4.3.1: {}