From f5d6c4063305affa8aaa59d1985136338aa3d7e9 Mon Sep 17 00:00:00 2001 From: David Wiggs Date: Tue, 31 Mar 2026 08:52:30 -0500 Subject: [PATCH] Add rules related to npm publish --- .../Security/CWE-353/MissingProvenanceFlag.md | 31 +++++++++++ .../Security/CWE-353/MissingProvenanceFlag.ql | 25 +++++++++ .../src/Security/CWE-798/NpmTokenInPublish.md | 35 ++++++++++++ .../src/Security/CWE-798/NpmTokenInPublish.ql | 54 +++++++++++++++++++ .../workflows/npm-publish-no-provenance.yml | 20 +++++++ .../workflows/npm-publish-with-provenance.yml | 28 ++++++++++ .../CWE-353/MissingProvenanceFlag.expected | 2 + .../CWE-353/MissingProvenanceFlag.qlref | 1 + .../workflows/npm-token-publish-safe.yml | 25 +++++++++ .../.github/workflows/npm-token-publish.yml | 34 ++++++++++++ .../CWE-798/NpmTokenInPublish.expected | 4 ++ .../Security/CWE-798/NpmTokenInPublish.qlref | 1 + 12 files changed, 260 insertions(+) create mode 100644 actions/ql/src/Security/CWE-353/MissingProvenanceFlag.md create mode 100644 actions/ql/src/Security/CWE-353/MissingProvenanceFlag.ql create mode 100644 actions/ql/src/Security/CWE-798/NpmTokenInPublish.md create mode 100644 actions/ql/src/Security/CWE-798/NpmTokenInPublish.ql create mode 100644 actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-no-provenance.yml create mode 100644 actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-with-provenance.yml create mode 100644 actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.expected create mode 100644 actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.qlref create mode 100644 actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish-safe.yml create mode 100644 actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish.yml create mode 100644 actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.expected create mode 100644 actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.qlref diff --git a/actions/ql/src/Security/CWE-353/MissingProvenanceFlag.md b/actions/ql/src/Security/CWE-353/MissingProvenanceFlag.md new file mode 100644 index 000000000000..3dbcad3b7faf --- /dev/null +++ b/actions/ql/src/Security/CWE-353/MissingProvenanceFlag.md @@ -0,0 +1,31 @@ +## Overview + +The `npm publish` command does not include the `--provenance` flag. Provenance attestation cryptographically links the published package to a specific source commit and workflow run, allowing consumers to verify where and how the package was built. + +## Recommendation + +Add `--provenance` to the `npm publish` command. This requires the workflow to have `id-token: write` permission and to run in a GitHub Actions environment. + +## Example + +### Incorrect Usage + +```yaml +- name: Publish + run: npm publish +``` + +### Correct Usage + +```yaml +permissions: + id-token: write + +- name: Publish + run: npm publish --provenance +``` + +## References + +- npm Docs: [Generating provenance statements](https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions). +- GitHub Blog: [Introducing npm package provenance](https://github.blog/security/supply-chain-security/introducing-npm-package-provenance/). diff --git a/actions/ql/src/Security/CWE-353/MissingProvenanceFlag.ql b/actions/ql/src/Security/CWE-353/MissingProvenanceFlag.ql new file mode 100644 index 000000000000..964a7c9ad7d6 --- /dev/null +++ b/actions/ql/src/Security/CWE-353/MissingProvenanceFlag.ql @@ -0,0 +1,25 @@ +/** + * @name npm publish missing --provenance flag + * @description The npm publish command does not include '--provenance'. Provenance attestation + * cryptographically links the published package to a specific source commit and + * workflow run. + * @kind problem + * @problem.severity warning + * @security-severity 5.0 + * @precision high + * @id actions/missing-provenance-flag + * @tags actions + * security + * supply-chain + * external/cwe/cwe-353 + */ + +import actions + +from Run run, string command +where + command = run.getScript().getACommand() and + command.regexpMatch("(?i).*\\bnpm\\s+publish\\b.*") and + not command.regexpMatch("(?i).*\\bnpm\\s+publish\\b.*--provenance\\b.*") +select run, + "npm publish command does not include '--provenance'. Add '--provenance' to cryptographically link the package to this source commit and workflow run." diff --git a/actions/ql/src/Security/CWE-798/NpmTokenInPublish.md b/actions/ql/src/Security/CWE-798/NpmTokenInPublish.md new file mode 100644 index 000000000000..854297bb0ad7 --- /dev/null +++ b/actions/ql/src/Security/CWE-798/NpmTokenInPublish.md @@ -0,0 +1,35 @@ +## Overview + +The publish step sets `NODE_AUTH_TOKEN` (or `NPM_TOKEN`) from a repository secret. This is a long-lived credential that can be stolen and used to publish malicious versions from outside the CI/CD pipeline, as demonstrated by the axios@1.14.1 supply chain attack. + +## Recommendation + +Remove `NODE_AUTH_TOKEN` from the publish step. Configure npm Trusted Publishing (OIDC) on npmjs.com, pointing to this repository and workflow. This eliminates the need for long-lived tokens entirely. + +## Example + +### Incorrect Usage + +```yaml +- name: Publish + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} +``` + +### Correct Usage + +Use npm Trusted Publishing (OIDC) instead of a long-lived token: + +```yaml +permissions: + id-token: write + +- name: Publish + run: npm publish --provenance +``` + +## References + +- npm Docs: [Generating provenance statements](https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions). +- GitHub Blog: [Introducing npm package provenance](https://github.blog/security/supply-chain-security/introducing-npm-package-provenance/). diff --git a/actions/ql/src/Security/CWE-798/NpmTokenInPublish.ql b/actions/ql/src/Security/CWE-798/NpmTokenInPublish.ql new file mode 100644 index 000000000000..68427cf04830 --- /dev/null +++ b/actions/ql/src/Security/CWE-798/NpmTokenInPublish.ql @@ -0,0 +1,54 @@ +/** + * @name Long-lived npm token used in publish step + * @description The publish step sets NODE_AUTH_TOKEN or NPM_TOKEN from a repository secret. + * This is a long-lived credential that can be stolen and used to publish malicious + * versions from outside the CI/CD pipeline. + * @kind problem + * @problem.severity error + * @security-severity 9.0 + * @precision high + * @id actions/npm-token-in-publish + * @tags actions + * security + * supply-chain + * external/cwe/cwe-798 + */ + +import actions + +/** + * Holds if `run` is a Run step whose script contains an `npm publish` or `yarn publish` command. + */ +predicate isNpmPublishStep(Run run) { + run.getScript().getACommand().regexpMatch("(?i).*\\bnpm\\s+publish\\b.*") or + run.getScript().getACommand().regexpMatch("(?i).*\\byarn\\s+publish\\b.*") +} + +/** + * Holds if `uses` is a UsesStep that calls a known npm publish action. + */ +predicate isNpmPublishUsesStep(UsesStep uses) { + uses.getCallee().matches(["JS-DevTools/npm-publish%", "js-devtools/npm-publish%"]) +} + +/** + * Holds if `expr` is an expression that references a secret (e.g. `secrets.NPM_TOKEN`). + */ +bindingset[exprValue] +predicate isSecretsReference(string exprValue) { + exprValue.regexpMatch("(?i)secrets\\..*") +} + +from Step publishStep, Env env, string envVarName, Expression secretExpr +where + ( + isNpmPublishStep(publishStep) or + isNpmPublishUsesStep(publishStep) + ) and + env = publishStep.getEnv() and + envVarName = ["NODE_AUTH_TOKEN", "NPM_TOKEN"] and + secretExpr = env.getEnvVarExpr(envVarName) and + isSecretsReference(secretExpr.getExpression()) +select secretExpr, + "Long-lived npm token '$@' is used in a publish step. Use npm Trusted Publishing (OIDC) instead.", + secretExpr, envVarName diff --git a/actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-no-provenance.yml b/actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-no-provenance.yml new file mode 100644 index 000000000000..aea247e8c8b4 --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-no-provenance.yml @@ -0,0 +1,20 @@ +name: npm-publish-no-provenance +on: + push: + tags: + - 'v*' +jobs: + publish-missing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - run: npm publish + publish-with-access: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm publish --access public diff --git a/actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-with-provenance.yml b/actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-with-provenance.yml new file mode 100644 index 000000000000..e5cf6807013f --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-353/.github/workflows/npm-publish-with-provenance.yml @@ -0,0 +1,28 @@ +name: npm-publish-with-provenance +on: + push: + tags: + - 'v*' +jobs: + publish-correct: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - run: npm publish --provenance + publish-with-access-and-provenance: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm publish --access public --provenance + not-a-publish-step: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm install diff --git a/actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.expected b/actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.expected new file mode 100644 index 000000000000..12296a8b9f9f --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.expected @@ -0,0 +1,2 @@ +| .github/workflows/npm-publish-no-provenance.yml:15:9:16:2 | Run Step | npm publish command does not include '--provenance'. Add '--provenance' to cryptographically link the package to this source commit and workflow run. | +| .github/workflows/npm-publish-no-provenance.yml:20:9:20:41 | Run Step | npm publish command does not include '--provenance'. Add '--provenance' to cryptographically link the package to this source commit and workflow run. | \ No newline at end of file diff --git a/actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.qlref b/actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.qlref new file mode 100644 index 000000000000..3c36221d1a97 --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-353/MissingProvenanceFlag.qlref @@ -0,0 +1 @@ +Security/CWE-353/MissingProvenanceFlag.ql diff --git a/actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish-safe.yml b/actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish-safe.yml new file mode 100644 index 000000000000..56cb021e62cd --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish-safe.yml @@ -0,0 +1,25 @@ +name: npm-publish-safe +on: + push: + tags: + - 'v*' +jobs: + publish-oidc: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - run: npm publish --provenance + publish-no-secret: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm publish + env: + SOME_OTHER_VAR: "hello" diff --git a/actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish.yml b/actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish.yml new file mode 100644 index 000000000000..307bc028f473 --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-798/.github/workflows/npm-token-publish.yml @@ -0,0 +1,34 @@ +name: npm-publish-with-token +on: + push: + tags: + - 'v*' +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: npm publish --provenance + env: + NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + publish-yarn: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: yarn publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + publish-action: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: JS-DevTools/npm-publish@v3 + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.expected b/actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.expected new file mode 100644 index 000000000000..7a0c2ac332a0 --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.expected @@ -0,0 +1,4 @@ +| .github/workflows/npm-token-publish.yml:17:29:17:52 | secrets.NPM_TOKEN | Long-lived npm token '$@' is used in a publish step. Use npm Trusted Publishing (OIDC) instead. | .github/workflows/npm-token-publish.yml:17:29:17:52 | secrets.NPM_TOKEN | NODE_AUTH_TOKEN | +| .github/workflows/npm-token-publish.yml:20:23:20:54 | secrets.NPM_PUBLISH_TOKEN | Long-lived npm token '$@' is used in a publish step. Use npm Trusted Publishing (OIDC) instead. | .github/workflows/npm-token-publish.yml:20:23:20:54 | secrets.NPM_PUBLISH_TOKEN | NPM_TOKEN | +| .github/workflows/npm-token-publish.yml:27:29:27:52 | secrets.NPM_TOKEN | Long-lived npm token '$@' is used in a publish step. Use npm Trusted Publishing (OIDC) instead. | .github/workflows/npm-token-publish.yml:27:29:27:52 | secrets.NPM_TOKEN | NODE_AUTH_TOKEN | +| .github/workflows/npm-token-publish.yml:34:29:34:52 | secrets.NPM_TOKEN | Long-lived npm token '$@' is used in a publish step. Use npm Trusted Publishing (OIDC) instead. | .github/workflows/npm-token-publish.yml:34:29:34:52 | secrets.NPM_TOKEN | NODE_AUTH_TOKEN | \ No newline at end of file diff --git a/actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.qlref b/actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.qlref new file mode 100644 index 000000000000..5652b514b668 --- /dev/null +++ b/actions/ql/test/query-tests/Security/CWE-798/NpmTokenInPublish.qlref @@ -0,0 +1 @@ +Security/CWE-798/NpmTokenInPublish.ql