From ce1a592d5f8463664c0b67c4effebcaeeb499bd9 Mon Sep 17 00:00:00 2001 From: yoshihiro999 <216183434+yoshihiro999@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:02:37 +0900 Subject: [PATCH] Harden Node archive Starlark generation --- knife | 4 ++ knife.d/update_node_archives.js | 93 +++++++++++++++++++++++----- knife.d/update_node_archives_test.js | 38 ++++++++++++ 3 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 knife.d/update_node_archives_test.js diff --git a/knife b/knife index f93413624..7cd75750c 100755 --- a/knife +++ b/knife @@ -176,6 +176,10 @@ function cmd_update_node_archives () { | sort_by(.date) | reverse | .[0].version ') latest_nov=$(echo "$latest_version" | sed 's/^v//') + if [[ ! "$latest_nov" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid Node.js version from release index: $latest_version" >&2 + exit 1 + fi versions+=("$latest_nov") done diff --git a/knife.d/update_node_archives.js b/knife.d/update_node_archives.js index fc1403c3e..553a60572 100755 --- a/knife.d/update_node_archives.js +++ b/knife.d/update_node_archives.js @@ -3,16 +3,52 @@ const crypto = require("crypto"); const https = require("https"); const fs = require("fs"); -if (process.argv.length < 3) { - console.error("Usage: node nodeChecksum.js "); - process.exit(1); -} - -const versions = process.argv[2].split(","); +let versions = []; const architectures = ["amd64", "arm64", "arm", "ppc64le", "s390x"]; +const NODE_VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/; +const ARCHIVE_SUFFIX_RE = /^(x64|arm64|armv7l|ppc64le|s390x)$/; +const DISTROLESS_ARCH_RE = /^(amd64|arm64|arm|ppc64le|s390x)$/; +const SHA256_RE = /^[0-9a-f]{64}$/; const nodeVersions = {}; +const starlarkString = (value) => { + return `"${String(value) + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"') + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t")}"`; +}; + +const validateNodeVersion = (version) => { + if (!NODE_VERSION_RE.test(version)) { + throw new Error(`Invalid Node.js version: ${version}`); + } + return version; +}; + +const validateArchiveSuffix = (suffix) => { + if (!ARCHIVE_SUFFIX_RE.test(suffix)) { + throw new Error(`Invalid Node.js archive suffix: ${suffix}`); + } + return suffix; +}; + +const validateDistrolessArch = (arch) => { + if (!DISTROLESS_ARCH_RE.test(arch)) { + throw new Error(`Invalid distroless architecture: ${arch}`); + } + return arch; +}; + +const validateSha256 = (sha256) => { + if (!SHA256_RE.test(sha256)) { + throw new Error(`Invalid SHA256 checksum: ${sha256}`); + } + return sha256; +}; + const calculateChecksum = (url) => { return new Promise((resolve, reject) => { https @@ -33,6 +69,7 @@ const calculateChecksum = (url) => { const fetchChecksums = async () => { for (const nodeVersion of versions) { + validateNodeVersion(nodeVersion); const major = parseInt(nodeVersion.split(".")[0]); nodeVersions[nodeVersion] = {}; await Promise.all( @@ -46,6 +83,7 @@ const fetchChecksums = async () => { } else if (key === "arm") { arch = "armv7l"; } + validateArchiveSuffix(arch); const url = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-${arch}.tar.gz`; try { const checksum = await calculateChecksum(url); @@ -61,7 +99,14 @@ const fetchChecksums = async () => { } }; -fetchChecksums().then(() => { +const main = async () => { + if (process.argv.length < 3) { + console.error("Usage: node nodeChecksum.js "); + process.exit(1); + } + versions = process.argv[2].split(","); + await fetchChecksums(); + let nodeArchives = `"node" BUILD_TMPL = """\\ @@ -172,16 +217,19 @@ def _node_impl(module_ctx): } const arch = nodeVersions[nodeVersion][key]; const url = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-${arch.suffix}.tar.gz`; + validateDistrolessArch(key); + validateArchiveSuffix(arch.suffix); + validateSha256(arch.checksum); nodeArchives += "\n"; nodeArchives += ` node_archive( - name = "nodejs${major}_${key}", - sha256 = "${arch.checksum}", - strip_prefix = "node-v${nodeVersion}-linux-${arch.suffix}/", - urls = ["${url}"], - version = "${nodeVersion}", - architecture = "${key}", + name = ${starlarkString(`nodejs${major}_${key}`)}, + sha256 = ${starlarkString(arch.checksum)}, + strip_prefix = ${starlarkString(`node-v${nodeVersion}-linux-${arch.suffix}/`)}, + urls = [${starlarkString(url)}], + version = ${starlarkString(nodeVersion)}, + architecture = ${starlarkString(key)}, control = "//nodejs:control", )`; } @@ -212,7 +260,7 @@ commandTests: continue; } nodeArchives += ` - "nodejs${major}_${arch}",`; + ${starlarkString(`nodejs${major}_${arch}`)},`; } } @@ -237,4 +285,19 @@ node = module_extension( console.error(err); } }); -}); +}; + +if (require.main === module) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} + +module.exports = { + starlarkString, + validateArchiveSuffix, + validateDistrolessArch, + validateNodeVersion, + validateSha256, +}; diff --git a/knife.d/update_node_archives_test.js b/knife.d/update_node_archives_test.js new file mode 100644 index 000000000..5cf08c575 --- /dev/null +++ b/knife.d/update_node_archives_test.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +const assert = require("node:assert/strict"); + +const { + starlarkString, + validateArchiveSuffix, + validateDistrolessArch, + validateNodeVersion, + validateSha256, +} = require("./update_node_archives.js"); + +assert.equal(validateNodeVersion("26.2.0"), "26.2.0"); +assert.throws( + () => validateNodeVersion('26.2.0" + "bad'), + /Invalid Node.js version/ +); +assert.throws(() => validateNodeVersion("v26.2.0"), /Invalid Node.js version/); +assert.throws(() => validateNodeVersion("26.2.0\n26.2.1"), /Invalid Node.js version/); + +assert.equal(validateArchiveSuffix("x64"), "x64"); +assert.throws(() => validateArchiveSuffix('x64" + fail("bad") + "'), /Invalid Node.js archive suffix/); + +assert.equal(validateDistrolessArch("amd64"), "amd64"); +assert.throws(() => validateDistrolessArch("amd64\nbad"), /Invalid distroless architecture/); + +assert.equal( + validateSha256("a".repeat(64)), + "a".repeat(64) +); +assert.throws(() => validateSha256("not-a-sha"), /Invalid SHA256 checksum/); + +assert.equal( + starlarkString('26.2.0" + "bad'), + '"26.2.0\\" + \\"bad"' +); +assert.equal(starlarkString("line1\nline2"), '"line1\\nline2"'); + +console.log("update_node_archives validation tests passed");