From 7066c895f610efbaa0a36bac22843cb05b5cf7d6 Mon Sep 17 00:00:00 2001 From: stuxf <70670632+stuxf@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:41:37 -0700 Subject: [PATCH] =?UTF-8?q?chore:=20harden=20npm=20supply=20chain=20?= =?UTF-8?q?=E2=80=94=20pin=20overrides,=20enforce=20npm=20ci,=20add=20igno?= =?UTF-8?q?re-scripts=20(#24838)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: harden npm supply chain — pin overrides, enforce npm ci, add ignore-scripts Replace open-ended >= version overrides with exact pins matching lockfile versions across all 6 package.json files. Remove dead overrides for packages not present in lockfiles. Switch CI and devcontainer from npm install to npm ci for deterministic lockfile-based installs. Add .npmrc to all 7 JS project directories with ignore-scripts=true (blocks postinstall RAT vectors like the axios@1.14.1 supply chain attack) and min-release-age=3d (refuses packages published <3 days ago, requires npm >=11.10). Remove Yarn-only resolutions field from docs/my-website. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: bump sharp to 0.33.5 in docs, add docs .npmrc sharp 0.32.x uses postinstall to download native binaries, which breaks with ignore-scripts=true. sharp 0.33+ distributes via optionalDependencies instead, making it compatible with the new .npmrc hardening. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: remove docs .npmrc to fix Vercel deploy Vercel's build for docs/my-website uses npm install which needs sharp 0.32.6's postinstall script. Since we don't control Vercel's build process, remove the .npmrc from docs rather than fight it. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: Dockerfile npm ci + nvm checksum verification - Replace npm install with npm ci in Dockerfile.non_root, Dockerfile.custom_ui, and spend-logs/Dockerfile for deterministic lockfile-based installs - Replace curl-pipe-bash nvm install with download-then-verify pattern in build_admin_ui.sh, build_ui.sh, and build_ui_custom_path.sh - Update nvm from v0.38.0 (2021) to v0.40.4 (Jan 2026) with SHA256 checksum verification before execution Co-Authored-By: Claude Opus 4.6 (1M context) * fix: macOS sha256sum compat + clarify min-release-age scope - Use shasum -a 256 fallback on macOS where sha256sum is unavailable - Clarify in .npmrc comments that min-release-age only protects local npm install, not npm ci (used in CI) Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .devcontainer/post-create.sh | 2 +- .github/workflows/test-litellm-ui-build.yml | 2 +- .npmrc | 5 ++ ci_cd/security_scans.sh | 2 +- docker/Dockerfile.custom_ui | 2 +- docker/Dockerfile.non_root | 2 +- docker/build_admin_ui.sh | 15 +++++- docs/my-website/package.json | 51 +++++++------------ litellm-js/proxy/.npmrc | 5 ++ litellm-js/spend-logs/.npmrc | 5 ++ litellm-js/spend-logs/Dockerfile | 2 +- litellm-js/spend-logs/package.json | 19 +------ package.json | 20 ++------ tests/proxy_admin_ui_tests/.npmrc | 5 ++ tests/proxy_admin_ui_tests/package.json | 19 +------ .../proxy_admin_ui_tests/ui_unit_tests/.npmrc | 5 ++ .../ui_unit_tests/package.json | 21 +++----- ui/litellm-dashboard/.npmrc | 5 ++ ui/litellm-dashboard/build_ui.sh | 16 +++++- ui/litellm-dashboard/build_ui_custom_path.sh | 16 +++++- ui/litellm-dashboard/package.json | 30 +++-------- 21 files changed, 117 insertions(+), 132 deletions(-) create mode 100644 .npmrc create mode 100644 litellm-js/proxy/.npmrc create mode 100644 litellm-js/spend-logs/.npmrc create mode 100644 tests/proxy_admin_ui_tests/.npmrc create mode 100644 tests/proxy_admin_ui_tests/ui_unit_tests/.npmrc create mode 100644 ui/litellm-dashboard/.npmrc diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index bd72e91a20..484baa9041 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -12,6 +12,6 @@ echo "[post-create] Generating Prisma client" poetry run prisma generate echo "[post-create] Installing npm dependencies" -cd ui/litellm-dashboard && npm install --no-audit --no-fund +cd ui/litellm-dashboard && npm ci echo "[post-create] Done" \ No newline at end of file diff --git a/.github/workflows/test-litellm-ui-build.yml b/.github/workflows/test-litellm-ui-build.yml index 6b0b3a413a..bef568298e 100644 --- a/.github/workflows/test-litellm-ui-build.yml +++ b/.github/workflows/test-litellm-ui-build.yml @@ -28,7 +28,7 @@ jobs: cache-dependency-path: ui/litellm-dashboard/package-lock.json - name: Install dependencies - run: npm install + run: npm ci - name: Build run: npm run build diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..168e81a1c4 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# Supply-chain hardening +# Packages needing lifecycle scripts: npm rebuild +ignore-scripts=true +# Protects local npm install only — npm ci (used in CI) ignores this +min-release-age=3d diff --git a/ci_cd/security_scans.sh b/ci_cd/security_scans.sh index 061e454465..ee33393c8b 100755 --- a/ci_cd/security_scans.sh +++ b/ci_cd/security_scans.sh @@ -160,7 +160,7 @@ run_grype_scans() { "CVE-2026-0775" # npm cli incorrect permission assignment - no fix available yet, npm is only used at build/prisma-generate time "GHSA-3ppc-4f35-3m26" # minimatch ReDoS via repeated wildcards - from nodejs_wheel bundled npm, not used in application runtime code "GHSA-83g3-92jg-28cx" # tar arbitrary file read/write via hardlink - from nodejs_wheel bundled npm, not used in application runtime code - "CVE-2026-25639" # axios - full fix requires 1.x major version bump; pinned to >=0.30.2 to clear other axios CVEs, upgrade to 1.x in follow-up + "CVE-2026-25639" # axios DoS via __proto__ in mergeConfig - transitive dev dep via @neondatabase/api-client, not imported in application code "CVE-2026-2297" # Python 3.13 SourcelessFileLoader audit hook bypass - no fix available in base image "GHSA-qffp-2rhf-9h96" # tar hardlink path traversal - from nodejs_wheel bundled npm, not used in application runtime code "CVE-2026-2673" # OpenSSL 3.6.1 TLS 1.3 key exchange group negotiation issue - no fix available yet diff --git a/docker/Dockerfile.custom_ui b/docker/Dockerfile.custom_ui index c1bd9a383f..9ab416944f 100644 --- a/docker/Dockerfile.custom_ui +++ b/docker/Dockerfile.custom_ui @@ -51,7 +51,7 @@ ENV UI_BASE_PATH="/prod/ui" # Build the UI with the specified UI_BASE_PATH WORKDIR /app/ui/litellm-dashboard -RUN npm install +RUN npm ci RUN UI_BASE_PATH=$UI_BASE_PATH npm run build # Create the destination directory diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index db3981fb7e..c224894779 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -47,7 +47,7 @@ RUN mkdir -p /var/lib/litellm/ui && \ if [ -f "/app/enterprise/enterprise_ui/enterprise_colors.json" ]; then \ cp /app/enterprise/enterprise_ui/enterprise_colors.json ./ui_colors.json; \ fi && \ - npm install --legacy-peer-deps && \ + npm ci && \ npm run build && \ cp -r /app/ui/litellm-dashboard/out/* /var/lib/litellm/ui/ && \ mkdir -p /var/lib/litellm/assets && \ diff --git a/docker/build_admin_ui.sh b/docker/build_admin_ui.sh index 5373ad0e3d..efb2bac353 100755 --- a/docker/build_admin_ui.sh +++ b/docker/build_admin_ui.sh @@ -40,11 +40,22 @@ else exit 1 fi fi -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash +NVM_VERSION="v0.40.4" +NVM_CHECKSUM="4b7412c49960c7d31e8df72da90c1fb5b8cccb419ac99537b737028d497aba4f" +NVM_SCRIPT=$(mktemp) +trap 'rm -f "$NVM_SCRIPT"' EXIT +curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "$NVM_SCRIPT" +if command -v sha256sum &>/dev/null; then + echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | sha256sum -c - +elif command -v shasum &>/dev/null; then + echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | shasum -a 256 -c - +else + echo "No sha256 tool found; cannot verify nvm checksum"; exit 1 +fi || { echo "nvm checksum verification failed"; exit 1; } +bash "$NVM_SCRIPT" source ~/.nvm/nvm.sh nvm install v18.17.0 nvm use v18.17.0 -npm install -g npm # copy _enterprise.json from this directory to /ui/litellm-dashboard, and rename it to ui_colors.json cp enterprise/enterprise_ui/enterprise_colors.json ui/litellm-dashboard/ui_colors.json diff --git a/docs/my-website/package.json b/docs/my-website/package.json index 4b41a7ef46..2802d24b57 100644 --- a/docs/my-website/package.json +++ b/docs/my-website/package.json @@ -48,27 +48,26 @@ "node": ">=16.14", "npm": ">=8.3.0" }, - "resolutions": { - "webpack-dev-server": ">=5.2.1", - "form-data": ">=4.0.4", - "mermaid": ">=11.10.0", - "gray-matter": "4.0.3", - "node-forge": ">=1.3.2" - }, "overrides": { - "webpack-dev-server": ">=5.2.1", - "form-data": ">=4.0.4", - "mermaid": ">=11.10.0", "gray-matter": "4.0.3", - "glob": ">=11.1.0", - "tar": ">=7.5.10", - "minimatch": ">=10.2.4", - "diff": ">=8.0.3", - "@isaacs/brace-expansion": ">=5.0.1", - "serialize-javascript": ">=7.0.3", - "node-forge": ">=1.3.2", - "mdast-util-to-hast": ">=13.2.1", - "lodash-es": ">=4.17.23", + "webpack-dev-server": "5.2.3", + "form-data": "4.0.5", + "mermaid": "11.12.1", + "minimatch": "10.2.4", + "serialize-javascript": "7.0.3", + "mdast-util-to-hast": "13.2.1", + "lodash-es": "4.17.23", + "@babel/traverse": "7.28.5", + "ws": "8.19.0", + "http-proxy-middleware": "3.0.5", + "tar-fs": "3.1.1", + "webpack-dev-middleware": "5.3.4", + "braces": "3.0.3", + "webpack": "5.105.3", + "serve-static": "2.2.1", + "path-to-regexp": "1.9.0", + "dompurify": "3.3.2", + "svgo": "4.0.1", "schema-utils@3": { "ajv": "6.14.0" }, @@ -83,18 +82,6 @@ }, "url-loader": { "ajv": "6.14.0" - }, - "@babel/traverse": ">=7.23.2", - "ws": ">=7.5.10", - "http-proxy-middleware": ">=2.0.9", - "tar-fs": ">=2.1.4", - "webpack-dev-middleware": ">=5.3.4", - "braces": ">=3.0.3", - "axios": "1.13.6", - "webpack": ">=5.94.0", - "serve-static": ">=1.16.0", - "path-to-regexp": ">=0.1.12", - "dompurify": ">=3.3.2", - "svgo": ">=3.3.3" + } } } diff --git a/litellm-js/proxy/.npmrc b/litellm-js/proxy/.npmrc new file mode 100644 index 0000000000..168e81a1c4 --- /dev/null +++ b/litellm-js/proxy/.npmrc @@ -0,0 +1,5 @@ +# Supply-chain hardening +# Packages needing lifecycle scripts: npm rebuild +ignore-scripts=true +# Protects local npm install only — npm ci (used in CI) ignores this +min-release-age=3d diff --git a/litellm-js/spend-logs/.npmrc b/litellm-js/spend-logs/.npmrc new file mode 100644 index 0000000000..168e81a1c4 --- /dev/null +++ b/litellm-js/spend-logs/.npmrc @@ -0,0 +1,5 @@ +# Supply-chain hardening +# Packages needing lifecycle scripts: npm rebuild +ignore-scripts=true +# Protects local npm install only — npm ci (used in CI) ignores this +min-release-age=3d diff --git a/litellm-js/spend-logs/Dockerfile b/litellm-js/spend-logs/Dockerfile index a325b5cbc9..5040dc74bf 100644 --- a/litellm-js/spend-logs/Dockerfile +++ b/litellm-js/spend-logs/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app COPY ./litellm-js/spend-logs/package*.json ./ # Install dependencies -RUN npm install +RUN npm ci # Install Prisma globally RUN npm install -g prisma diff --git a/litellm-js/spend-logs/package.json b/litellm-js/spend-logs/package.json index 2ac81ee489..8bccbdba27 100644 --- a/litellm-js/spend-logs/package.json +++ b/litellm-js/spend-logs/package.json @@ -9,22 +9,5 @@ "devDependencies": { "@types/node": "^20.11.17", "tsx": "^4.7.1" - }, - "overrides": { - "glob": ">=11.1.0", - "tar": ">=7.5.10", - "minimatch": ">=10.2.4", - "diff": ">=8.0.3", - "@isaacs/brace-expansion": ">=5.0.1", - "@babel/traverse": ">=7.23.2", - "ws": ">=7.5.10", - "http-proxy-middleware": ">=2.0.9", - "tar-fs": ">=2.1.4", - "webpack-dev-middleware": ">=5.3.4", - "braces": ">=3.0.3", - "axios": "1.13.6", - "webpack": ">=5.94.0", - "serve-static": ">=1.16.0", - "path-to-regexp": ">=0.1.12" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index e44ebd4068..9a7ebf4ae0 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,10 @@ "jest": "^29.7.0" }, "overrides": { - "glob": ">=11.1.0", - "tar": ">=7.5.11", - "minimatch": ">=10.2.4", - "diff": ">=8.0.3", - "@isaacs/brace-expansion": ">=5.0.1", - "@babel/traverse": ">=7.23.2", - "ws": ">=7.5.10", - "http-proxy-middleware": ">=2.0.9", - "tar-fs": ">=2.1.4", - "webpack-dev-middleware": ">=5.3.4", - "braces": ">=3.0.3", - "axios": "1.13.6", - "webpack": ">=5.94.0", - "serve-static": ">=1.16.0", - "path-to-regexp": ">=0.1.12" + "glob": "13.0.0", + "minimatch": "10.1.1", + "@isaacs/brace-expansion": "5.0.0", + "@babel/traverse": "7.28.5", + "braces": "3.0.3" } } diff --git a/tests/proxy_admin_ui_tests/.npmrc b/tests/proxy_admin_ui_tests/.npmrc new file mode 100644 index 0000000000..168e81a1c4 --- /dev/null +++ b/tests/proxy_admin_ui_tests/.npmrc @@ -0,0 +1,5 @@ +# Supply-chain hardening +# Packages needing lifecycle scripts: npm rebuild +ignore-scripts=true +# Protects local npm install only — npm ci (used in CI) ignores this +min-release-age=3d diff --git a/tests/proxy_admin_ui_tests/package.json b/tests/proxy_admin_ui_tests/package.json index 410a0f5eca..20dfed7a8a 100644 --- a/tests/proxy_admin_ui_tests/package.json +++ b/tests/proxy_admin_ui_tests/package.json @@ -10,22 +10,5 @@ "devDependencies": { "@playwright/test": "^1.47.2", "@types/node": "^22.5.5" - }, - "overrides": { - "glob": ">=11.1.0", - "tar": ">=7.5.10", - "minimatch": ">=10.2.4", - "diff": ">=8.0.3", - "@isaacs/brace-expansion": ">=5.0.1", - "@babel/traverse": ">=7.23.2", - "ws": ">=7.5.10", - "http-proxy-middleware": ">=2.0.9", - "tar-fs": ">=2.1.4", - "webpack-dev-middleware": ">=5.3.4", - "braces": ">=3.0.3", - "axios": "1.13.6", - "webpack": ">=5.94.0", - "serve-static": ">=1.16.0", - "path-to-regexp": ">=0.1.12" } -} \ No newline at end of file +} diff --git a/tests/proxy_admin_ui_tests/ui_unit_tests/.npmrc b/tests/proxy_admin_ui_tests/ui_unit_tests/.npmrc new file mode 100644 index 0000000000..168e81a1c4 --- /dev/null +++ b/tests/proxy_admin_ui_tests/ui_unit_tests/.npmrc @@ -0,0 +1,5 @@ +# Supply-chain hardening +# Packages needing lifecycle scripts: npm rebuild +ignore-scripts=true +# Protects local npm install only — npm ci (used in CI) ignores this +min-release-age=3d diff --git a/tests/proxy_admin_ui_tests/ui_unit_tests/package.json b/tests/proxy_admin_ui_tests/ui_unit_tests/package.json index 2c8aab2dc4..c6a3e6e260 100644 --- a/tests/proxy_admin_ui_tests/ui_unit_tests/package.json +++ b/tests/proxy_admin_ui_tests/ui_unit_tests/package.json @@ -24,20 +24,11 @@ "react-dom": "^18.2.0" }, "overrides": { - "glob": ">=11.1.0", - "tar": ">=7.5.10", - "minimatch": ">=10.2.4", - "diff": ">=8.0.3", - "@isaacs/brace-expansion": ">=5.0.1", - "@babel/traverse": ">=7.23.2", - "ws": ">=7.5.10", - "http-proxy-middleware": ">=2.0.9", - "tar-fs": ">=2.1.4", - "webpack-dev-middleware": ">=5.3.4", - "braces": ">=3.0.3", - "axios": "1.13.6", - "webpack": ">=5.94.0", - "serve-static": ">=1.16.0", - "path-to-regexp": ">=0.1.12" + "glob": "13.0.0", + "minimatch": "10.1.1", + "@isaacs/brace-expansion": "5.0.0", + "@babel/traverse": "7.28.5", + "ws": "8.18.3", + "braces": "3.0.3" } } \ No newline at end of file diff --git a/ui/litellm-dashboard/.npmrc b/ui/litellm-dashboard/.npmrc new file mode 100644 index 0000000000..168e81a1c4 --- /dev/null +++ b/ui/litellm-dashboard/.npmrc @@ -0,0 +1,5 @@ +# Supply-chain hardening +# Packages needing lifecycle scripts: npm rebuild +ignore-scripts=true +# Protects local npm install only — npm ci (used in CI) ignores this +min-release-age=3d diff --git a/ui/litellm-dashboard/build_ui.sh b/ui/litellm-dashboard/build_ui.sh index cd6ec90190..aa346c12ed 100755 --- a/ui/litellm-dashboard/build_ui.sh +++ b/ui/litellm-dashboard/build_ui.sh @@ -2,8 +2,20 @@ # Check if nvm is not installed if ! command -v nvm &> /dev/null; then - # Install nvm - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash + # Install nvm with checksum verification + NVM_VERSION="v0.40.4" + NVM_CHECKSUM="4b7412c49960c7d31e8df72da90c1fb5b8cccb419ac99537b737028d497aba4f" + NVM_SCRIPT=$(mktemp) + trap 'rm -f "$NVM_SCRIPT"' EXIT + curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "$NVM_SCRIPT" + if command -v sha256sum &>/dev/null; then + echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | sha256sum -c - + elif command -v shasum &>/dev/null; then + echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | shasum -a 256 -c - + else + echo "No sha256 tool found; cannot verify nvm checksum"; exit 1 + fi || { echo "nvm checksum verification failed"; exit 1; } + bash "$NVM_SCRIPT" # Source nvm script in the current session export NVM_DIR="$HOME/.nvm" diff --git a/ui/litellm-dashboard/build_ui_custom_path.sh b/ui/litellm-dashboard/build_ui_custom_path.sh index f947f87d3b..a92927f8ea 100755 --- a/ui/litellm-dashboard/build_ui_custom_path.sh +++ b/ui/litellm-dashboard/build_ui_custom_path.sh @@ -12,8 +12,20 @@ UI_BASE_PATH="$1" # Check if nvm is not installed if ! command -v nvm &> /dev/null; then - # Install nvm - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash + # Install nvm with checksum verification + NVM_VERSION="v0.40.4" + NVM_CHECKSUM="4b7412c49960c7d31e8df72da90c1fb5b8cccb419ac99537b737028d497aba4f" + NVM_SCRIPT=$(mktemp) + trap 'rm -f "$NVM_SCRIPT"' EXIT + curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "$NVM_SCRIPT" + if command -v sha256sum &>/dev/null; then + echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | sha256sum -c - + elif command -v shasum &>/dev/null; then + echo "${NVM_CHECKSUM} ${NVM_SCRIPT}" | shasum -a 256 -c - + else + echo "No sha256 tool found; cannot verify nvm checksum"; exit 1 + fi || { echo "nvm checksum verification failed"; exit 1; } + bash "$NVM_SCRIPT" # Source nvm script in the current session export NVM_DIR="$HOME/.nvm" diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 7817481323..7b763eaa66 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -82,28 +82,14 @@ "vitest": "^3.2.4" }, "overrides": { - "diff": ">=8.0.3", - "prismjs": ">=1.30.0", - "webpack-dev-server": ">=5.2.1", - "mermaid": ">=11.10.0", - "js-yaml": ">=4.1.1", - "glob": ">=11.1.0", - "tar": ">=7.5.11", - "minimatch": ">=10.2.4", - "@isaacs/brace-expansion": ">=5.0.1", - "node-forge": ">=1.3.2", - "lodash-es": ">=4.17.23", - "lodash": ">=4.17.23", - "@babel/traverse": ">=7.23.2", - "ws": ">=7.5.10", - "http-proxy-middleware": ">=2.0.9", - "tar-fs": ">=2.1.4", - "webpack-dev-middleware": ">=5.3.4", - "braces": ">=3.0.3", - "axios": "1.13.6", - "webpack": ">=5.94.0", - "serve-static": ">=1.16.0", - "path-to-regexp": ">=0.1.12" + "prismjs": "1.30.0", + "js-yaml": "4.1.1", + "glob": "13.0.0", + "minimatch": "10.2.4", + "lodash": "4.17.23", + "ws": "8.19.0", + "braces": "3.0.3", + "axios": "1.13.6" }, "engines": { "node": ">=18.17.0",