From 7c2c68d20d402ed6a201ada70a81341941093140 Mon Sep 17 00:00:00 2001 From: Chiranjib Swain Date: Thu, 2 Jul 2026 23:44:10 +0530 Subject: [PATCH 1/2] docs: Update caching recommendations to mitigate cache poisoning risks (#1567) * docs: Update caching recommendations to mitigate cache poisoning risks * docs: Disable automatic npm caching to mitigate cache poisoning risks --- README.md | 3 +++ docs/advanced-usage.md | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 715026de..de8d2ae3 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ steps: - uses: actions/setup-node@v6 with: node-version: 24 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -217,6 +218,7 @@ jobs: uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -232,6 +234,7 @@ uses: actions/setup-node@v6 with: token: ${{ secrets.GH_DOTCOM_TOKEN }} node-version: 24 + package-manager-cache: false # Disable automatic npm caching if not required ``` If the runner is not able to access github.com, any Nodejs versions requested during a workflow run must come from the runner's tool cache. See "[Setting up the tool cache on self-hosted runners without internet access](https://docs.github.com/en/enterprise-server@3.2/admin/github-actions/managing-access-to-actions-from-githubcom/setting-up-the-tool-cache-on-self-hosted-runners-without-internet-access)" for more information. diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 5f0edfb0..3b4417f1 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -69,6 +69,7 @@ steps: with: node-version: '24' check-latest: true + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -86,6 +87,7 @@ steps: - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -131,6 +133,7 @@ jobs: with: node-version: '24' architecture: 'x64' # optional, x64 or x86. If not specified, x64 will be used by default + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -151,6 +154,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-v8-canary' # it will install the latest v8 canary release for node 24.0.0 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -166,6 +170,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24-v8-canary' # it will install the latest v8 canary release for node 24 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -182,6 +187,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: 'v24.0.0-v8-canary2025030537242e55ac' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -202,6 +208,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24-nightly' # it will install the latest nightly release for node 24 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -218,6 +225,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-nightly' # it will install the latest nightly release for node 24.0.0 + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -234,6 +242,7 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-nightly202505066102159fa1' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -252,11 +261,12 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '24.0.0-rc.4' + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` -**Note:** Unlike nightly versions, which support version range specifiers, you must specify the exact version for a release candidate: `24.0.0-rc.4`. +**Note**: Unlike nightly versions, which support version range specifiers, you must specify the exact version for a release candidate: `24.0.0-rc.4`. ## Caching packages data The action follows [actions/cache](https://github.com/actions/cache/blob/main/examples.md#node---npm) guidelines, and caches global cache on the machine instead of `node_modules`, so cache can be reused between different Node.js versions. @@ -344,6 +354,7 @@ steps: uses: actions/setup-node@v6 with: node-version: '24' + package-manager-cache: false # Disable automatic npm caching if not required - name: Normalize runner architecture shell: bash @@ -404,6 +415,7 @@ jobs: with: node-version: ${{ matrix.node_version }} architecture: ${{ matrix.architecture }} + package-manager-cache: false # Disable automatic npm caching if not required - run: npm ci - run: npm test ``` @@ -416,6 +428,7 @@ steps: with: node-version: '24.x' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: npm ci - run: npm publish env: @@ -423,6 +436,7 @@ steps: - uses: actions/setup-node@v6 with: registry-url: 'https://npm.pkg.github.com' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -436,6 +450,7 @@ steps: with: node-version: '24.x' registry-url: + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: yarn install --frozen-lockfile - run: yarn publish env: @@ -443,6 +458,7 @@ steps: - uses: actions/setup-node@v6 with: registry-url: 'https://npm.pkg.github.com' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: yarn publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -456,6 +472,7 @@ steps: with: node-version: '24.x' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk # Skip post-install scripts here, as a malicious # script could steal NODE_AUTH_TOKEN. - run: npm ci --ignore-scripts @@ -475,6 +492,7 @@ steps: - uses: actions/setup-node@v6 with: node-version: '24.x' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - name: Setup .yarnrc.yml run: | yarn config set npmScopes.my-org.npmRegistryServer "https://npm.pkg.github.com" @@ -505,6 +523,8 @@ Trusted publishing requires a compatible npm version: You must also configure a **Trusted Publisher** in npm for your package/scope that matches your GitHub repository and workflow (and optional environment, if used). +> **Note**: In publishing workflows, set `package-manager-cache: false` because setup-node enables npm caching automatically when `package.json` specifies npm via `packageManager` or `devEngines.packageManager` (see [Running without a lockfile](#running-without-a-lockfile)), and a poisoned cache may expose credentials (including OIDC tokens) to attacker-controlled code. + ### Example workflow ```yaml @@ -519,6 +539,7 @@ You must also configure a **Trusted Publisher** in npm for your package/scope th with: node-version: '24' registry-url: 'https://registry.npmjs.org' + package-manager-cache: false # Disable automatic npm dependency caching to reduce cache poisoning risk - run: npm ci - run: npm run build --if-present @@ -542,4 +563,5 @@ The token will be passed in the `Authorization` header. node-version: '24.x' mirror: 'https://nodejs.org/dist' mirror-token: 'your-mirror-token' + cache-package-manager: false # Disable automatic npm caching if not required ``` From 04be95cf3511ea51ebf9f224ddfb99cc7ab87cd4 Mon Sep 17 00:00:00 2001 From: gowridurgad <159780674+gowridurgad@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:27:48 +0530 Subject: [PATCH 2/2] Add cache-primary-key and cache-matched-key as outputs (#1577) * Add cache-primary-key and cache-matched-key as outputs * Refine cache output descriptions --------- Co-authored-by: gowridurgad --- __tests__/cache-restore.test.ts | 20 +++++++++++++++++++- action.yml | 4 ++++ dist/setup/index.js | 3 +++ src/cache-restore.ts | 3 +++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/__tests__/cache-restore.test.ts b/__tests__/cache-restore.test.ts index 3145636a..d0ff0879 100644 --- a/__tests__/cache-restore.test.ts +++ b/__tests__/cache-restore.test.ts @@ -137,6 +137,7 @@ describe('cache-restore', () => { ] as const)( 'restored dependencies for %s', async (packageManager, toolVersion, fileHash) => { + const expectedCacheKey = `node-cache-${platform}-${arch}-${packageManager}-${fileHash}`; // Set workspace to the appropriate fixture folder setWorkspaceFor(packageManager); getCommandOutputSpy.mockImplementation((command: string) => { @@ -150,12 +151,20 @@ describe('cache-restore', () => { await restoreCache(packageManager, ''); expect(hashFilesSpy).toHaveBeenCalled(); expect(infoSpy).toHaveBeenCalledWith( - `Cache restored from key: node-cache-${platform}-${arch}-${packageManager}-${fileHash}` + `Cache restored from key: ${expectedCacheKey}` ); expect(infoSpy).not.toHaveBeenCalledWith( `${packageManager} cache is not found` ); expect(setOutputSpy).toHaveBeenCalledWith('cache-hit', true); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-primary-key', + expectedCacheKey + ); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-matched-key', + expectedCacheKey + ); } ); }); @@ -169,6 +178,7 @@ describe('cache-restore', () => { ] as const)( 'dependencies are changed %s', async (packageManager, toolVersion, fileHash) => { + const expectedCacheKey = `node-cache-${platform}-${arch}-${packageManager}-${fileHash}`; // Set workspace to the appropriate fixture folder setWorkspaceFor(packageManager); getCommandOutputSpy.mockImplementation((command: string) => { @@ -186,6 +196,14 @@ describe('cache-restore', () => { `${packageManager} cache is not found` ); expect(setOutputSpy).toHaveBeenCalledWith('cache-hit', false); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-primary-key', + expectedCacheKey + ); + expect(setOutputSpy).toHaveBeenCalledWith( + 'cache-matched-key', + undefined + ); } ); }); diff --git a/action.yml b/action.yml index 79813721..dc1cefd5 100644 --- a/action.yml +++ b/action.yml @@ -34,6 +34,10 @@ inputs: outputs: cache-hit: description: 'A boolean value to indicate if a cache was hit.' + cache-primary-key: + description: 'The key used to restore and save the cache.' + cache-matched-key: + description: 'The key of the cache that was restored. Empty when no cache is found.' node-version: description: 'The installed node version.' runs: diff --git a/dist/setup/index.js b/dist/setup/index.js index bb0a1e93..b6df902e 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -57021,6 +57021,7 @@ const restoreCache = async (packageManager, cacheDependencyPath) => { const primaryKey = `${keyPrefix}-${fileHash}`; core.debug(`primary key is ${primaryKey}`); core.saveState(constants_1.State.CachePrimaryKey, primaryKey); + core.setOutput('cache-primary-key', primaryKey); const isManagedByYarnBerry = await (0, cache_utils_1.repoHasYarnBerryManagedDependencies)(packageManagerInfo, cacheDependencyPath); let cacheKey; if (isManagedByYarnBerry) { @@ -57031,6 +57032,8 @@ const restoreCache = async (packageManager, cacheDependencyPath) => { cacheKey = await cache.restoreCache(cachePaths, primaryKey); } core.setOutput('cache-hit', Boolean(cacheKey)); + core.setOutput('cache-matched-key', cacheKey); + core.debug(`cache-matched-key is ${cacheKey}`); if (!cacheKey) { core.info(`${packageManager} cache is not found`); return; diff --git a/src/cache-restore.ts b/src/cache-restore.ts index af12ad83..eb620ac7 100644 --- a/src/cache-restore.ts +++ b/src/cache-restore.ts @@ -45,6 +45,7 @@ export const restoreCache = async ( core.debug(`primary key is ${primaryKey}`); core.saveState(State.CachePrimaryKey, primaryKey); + core.setOutput('cache-primary-key', primaryKey); const isManagedByYarnBerry = await repoHasYarnBerryManagedDependencies( packageManagerInfo, @@ -61,6 +62,8 @@ export const restoreCache = async ( } core.setOutput('cache-hit', Boolean(cacheKey)); + core.setOutput('cache-matched-key', cacheKey); + core.debug(`cache-matched-key is ${cacheKey}`); if (!cacheKey) { core.info(`${packageManager} cache is not found`);