Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions actions/ql/src/Security/CWE-353/MissingProvenanceFlag.md
Original file line number Diff line number Diff line change
@@ -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/).
25 changes: 25 additions & 0 deletions actions/ql/src/Security/CWE-353/MissingProvenanceFlag.ql
Original file line number Diff line number Diff line change
@@ -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."

Check warning

Code scanning / CodeQL

Alert message style violation Warning

Alert message should start with a capital letter.
35 changes: 35 additions & 0 deletions actions/ql/src/Security/CWE-798/NpmTokenInPublish.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overview states the token comes from a "repository secret", but the secret context also includes environment/org secrets. Consider using more accurate wording like "a GitHub Actions secret (secrets.*)".

This issue also appears on line 7 of the same file.

Suggested change
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.
The publish step sets `NODE_AUTH_TOKEN` (or `NPM_TOKEN`) from a GitHub Actions secret (`secrets.*`). 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.

Copilot uses AI. Check for mistakes.

## 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/).
54 changes: 54 additions & 0 deletions actions/ql/src/Security/CWE-798/NpmTokenInPublish.ql
Original file line number Diff line number Diff line change
@@ -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.
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query help text says the token is sourced from a "repository secret", but this query matches any secrets.* reference (which may be repository, environment, or organization). Consider rewording to "a GitHub Actions secret" (or similar) to avoid being misleading.

Suggested change
* @description The publish step sets NODE_AUTH_TOKEN or NPM_TOKEN from a repository secret.
* @description The publish step sets NODE_AUTH_TOKEN or NPM_TOKEN from a GitHub Actions secret.

Copilot uses AI. Check for mistakes.
* 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.",

Check warning

Code scanning / CodeQL

Alert message style violation Warning

Don't quote substitutions in alert messages.
secretExpr, envVarName

Check warning

Code scanning / CodeQL

Alert message style violation Warning

Don't repeat the alert location as a link.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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. |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Security/CWE-353/MissingProvenanceFlag.ql

Check warning

Code scanning / CodeQL

Query test without inline test expectations Warning test

Query test does not use inline test expectations.
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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 }}
Original file line number Diff line number Diff line change
@@ -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 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Security/CWE-798/NpmTokenInPublish.ql

Check warning

Code scanning / CodeQL

Query test without inline test expectations Warning test

Query test does not use inline test expectations.
Loading