Skip to content
Closed
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ This action runs the [Microsoft Security DevOps CLI](https://aka.ms/msdo-nuget)

See [action.yml](action.yml)

## Node.js runtime security

The action uses the GitHub Actions runner's `node24` interpreter, not the Node.js selected by `actions/setup-node` or the shell `PATH`.

The CLI installer can decode Brotli-compressed NuGet metadata through its dependencies. [CVE-2026-111497](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases) affects the native Brotli decoder in Node.js, so updating only JavaScript dependencies does not patch the interpreter.

Before CLI installation, the action logs its actual Node.js version, executable path, and Brotli version. It then rejects unpatched or unverified runtimes. The accepted stable upstream release lines start at Node.js **22.23.4**, **24.22.1**, and **26.12.1**, respectively. Older, end-of-life, prerelease, and unrecognized release lines are rejected. Vendor backports with older version numbers require a reviewed update to this policy; they are not accepted automatically.

For GitHub-hosted runners, the hosted provider must supply the patched action interpreter. For self-hosted runners, the runner operator must update the runner and its bundled `node24` interpreter. Installing a patched Node.js elsewhere on the host is insufficient.

After rollout, verify the executable and version reported by a subsequent legitimate action run, and confirm that the action completes successfully. Record the action ref and runner identity with that evidence. A local test, a `node --version` shell step, or the `node24` metadata declaration alone does not verify the deployed action interpreter.

## Basic

Run **Microsoft Security DevOps (MSDO)** with the default policy and recommended tools.
Expand Down
3 changes: 3 additions & 0 deletions lib/v1/msdo.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const core = __importStar(require("@actions/core"));
const msdo_helpers_1 = require("./msdo-helpers");
const client = __importStar(require("@microsoft/security-devops-actions-toolkit/msdo-client"));
const common = __importStar(require("@microsoft/security-devops-actions-toolkit/msdo-common"));
const node_runtime_1 = require("./node-runtime");
class MicrosoftSecurityDevOps {
constructor() {
this.succeedOnError = false;
Expand All @@ -51,6 +52,8 @@ class MicrosoftSecurityDevOps {
}
runMain() {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Node.js action runtime: ${process.versions.node}; executable: ${process.execPath}; Brotli: ${process.versions.brotli}`);
(0, node_runtime_1.assertPatchedNodeRuntime)(process.versions.node);
core.debug('MicrosoftSecurityDevOps.runMain - Running MSDO...');
let args = undefined;
let existingFilename = core.getInput('existingFilename');
Expand Down
22 changes: 22 additions & 0 deletions lib/v1/node-runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertPatchedNodeRuntime = assertPatchedNodeRuntime;
const minimumNodeVersions = {
22: [23, 4],
24: [22, 1],
26: [12, 1]
};
function assertPatchedNodeRuntime(version) {
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
if (match) {
const [major, minor, patch] = match.slice(1).map(Number);
const minimum = minimumNodeVersions[major];
if (minimum && (minor > minimum[0] || (minor === minimum[0] && patch >= minimum[1]))) {
return;
}
}
throw new Error(`CVE-2026-111497: Node.js ${version} is not a verified patched release. ` +
'Use Node.js 22.23.4+, 24.22.1+, or 26.12.1+ within those supported release lines. ' +
'Update the runner-managed action interpreter through the hosted provider or self-hosted runner operator; ' +
'actions/setup-node and PATH changes do not update this executable.');
}
4 changes: 4 additions & 0 deletions src/v1/msdo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IMicrosoftSecurityDevOps } from './msdo-interface';
import { Tools } from './msdo-helpers';
import * as client from '@microsoft/security-devops-actions-toolkit/msdo-client';
import * as common from '@microsoft/security-devops-actions-toolkit/msdo-common';
import { assertPatchedNodeRuntime } from './node-runtime';

/*
* Microsoft Security DevOps analyzers runner.
Expand All @@ -23,6 +24,9 @@ export class MicrosoftSecurityDevOps implements IMicrosoftSecurityDevOps {
}

public async runMain() {
core.info(`Node.js action runtime: ${process.versions.node}; executable: ${process.execPath}; Brotli: ${process.versions.brotli}`);
assertPatchedNodeRuntime(process.versions.node);

core.debug('MicrosoftSecurityDevOps.runMain - Running MSDO...');

let args: string[] = undefined;
Expand Down
25 changes: 25 additions & 0 deletions src/v1/node-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const minimumNodeVersions: Record<number, readonly [number, number]> = {
22: [23, 4],
24: [22, 1],
26: [12, 1]
};

export function assertPatchedNodeRuntime(version: string): void {
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);

if (match) {
const [major, minor, patch] = match.slice(1).map(Number);
const minimum = minimumNodeVersions[major];

if (minimum && (minor > minimum[0] || (minor === minimum[0] && patch >= minimum[1]))) {
return;
}
}

throw new Error(
`CVE-2026-111497: Node.js ${version} is not a verified patched release. ` +
'Use Node.js 22.23.4+, 24.22.1+, or 26.12.1+ within those supported release lines. ' +
'Update the runner-managed action interpreter through the hosted provider or self-hosted runner operator; ' +
'actions/setup-node and PATH changes do not update this executable.'
);
}
84 changes: 84 additions & 0 deletions test/node-runtime.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import assert from 'assert';
import sinon from 'sinon';
import * as core from '@actions/core';
import * as client from '@microsoft/security-devops-actions-toolkit/msdo-client';
import { MicrosoftSecurityDevOps } from '../lib/v1/msdo';

describe('MSDO action Node.js runtime', () => {
let sandbox: sinon.SinonSandbox;
let runStub: sinon.SinonStub;
let infoStub: sinon.SinonStub;

beforeEach(() => {
sandbox = sinon.createSandbox();
sandbox.stub(core, 'getInput').returns('');
sandbox.stub(core, 'debug');
infoStub = sandbox.stub(core, 'info');
runStub = sandbox.stub(client, 'run').resolves();
});

afterEach(() => {
sandbox.restore();
});

for (const version of [
'22.23.4', '22.23.5', '22.24.0',
'24.22.1', '24.22.2', '24.23.0',
'26.12.1', '26.12.2', '26.13.0'
]) {
it(`preserves CLI execution on patched Node.js ${version}`, async () => {
sandbox.stub(process.versions, 'node').value(version);

await new MicrosoftSecurityDevOps().runMain();

sinon.assert.calledOnceWithExactly(
runStub,
['run', '-p', 'GitHub', '--github'],
'microsoft/security-devops-action'
);
});
}

for (const version of [
'22.23.3', '22.22.99',
'24.22.0', '24.21.99',
'26.12.0', '26.11.99',
'18.20.8', '20.20.0', '23.11.0', '25.9.0', '27.0.0',
'24.22.1-rc.1', '24.22', '24.22.1.0', ''
]) {
it(`rejects unpatched or unverified Node.js "${version}" before CLI installation`, async () => {
sandbox.stub(process.versions, 'node').value(version);

await assert.rejects(
new MicrosoftSecurityDevOps().runMain(),
/CVE-2026-111497/
);
sinon.assert.notCalled(runStub);
});
}

it('logs the actual action interpreter and Brotli version', async () => {
sandbox.stub(process.versions, 'node').value('24.22.1');

await new MicrosoftSecurityDevOps().runMain();

sinon.assert.calledWithExactly(
infoStub,
`Node.js action runtime: ${process.versions.node}; executable: ${process.execPath}; Brotli: ${process.versions.brotli}`
);
});

it('reports the rejected runtime and explains runner remediation', async () => {
sandbox.stub(process.versions, 'node').value('24.22.0');

await assert.rejects(
new MicrosoftSecurityDevOps().runMain(),
/runner-managed.*actions\/setup-node/
);
sinon.assert.calledWithExactly(
infoStub,
`Node.js action runtime: ${process.versions.node}; executable: ${process.execPath}; Brotli: ${process.versions.brotli}`
);
sinon.assert.notCalled(runStub);
});
});
Loading