Axios compromised, does this affect nodered?

Hello, not sure if this would be the appropriate place to ask this question. I just found out about a hack that happened regarding npm/axios. Since I don't use npm all that much I figured I'd be fine, but then I remembered I use nodered with home assistant, now I am wondering is nodered affected.

Resources


Hi @HybridZach

The core of Node-RED does not directly use axios, but it does get installed via the node-red-admin dependency. The dependency is pinned to the 1.13.x range, so would not have been affected by this issue.

I cannot vouch for all 3rd party nodes however.

Nick

1 Like

We had similar concerns I checked the package.json for node-red-admin on NPM and found that ^1.13.5 allows any version >=1.13.5 <2.0.0, so it will also pick up 1.14.x, 1.15.x, etc. To pin to 1.13.x only, use ~1.13.5, which resolves to >=1.13.5 <1.14.0.

@malee you are right - I overlooked that. I'll get node-red-admin updated to pin its dependencies.

If a package-lock.json is provided then does that pin it to the version in there? I thought it would do that.

I believe that only 1 minor 1.14 version is impacted. As it has been found, I would expect that it has been fixed already. I would also expect the Axios project to improve their repo security. So really, any pinning is now almost certainly too late to have any impact.

This is another reminder to folk who create anything that has linked packages, no matter what platform and language they use to up their game around package and dependency security.

Thankfully, there are plenty of free tools around that will help.

It fixes the initial installation I think but I don't believe that it actually pins the version - so I'm fairly sure that doing npm updates later on will pick up whatever version allowed in package.json.

@knolleary, it might be worth having a test script to hand that lets people easily check for specific npm packages in their own installations and reports what uses it and the version(s) installed?

This is a good breakdown on the trojan, it was a really well targeted attack. Brief though, only 3 hours between midnight and 3am (UTC). I figure they had a specific target in mind...

In general* we do pin dependencies to specific versions. There are a few instances where that isn't a case. For example, the recent PR that updated the axios dependency introduced the ^ and I didn't flag it in review.

I have just published a new version of node-red-admin that pins all dependencies to their latest versions. I'll review the node-red core dependencies to ensure they are pinned consistently.

I appreciate this is closing the door after the horse has bolted, but that's at least better than not taking any action.

1 Like

Just looking to see if I can create a useful cli tool for checking installed packages.

Not sure if helpful but the link in my OP has commands for checking for fragments / if you have been compromised.

D2 also includes axios.

Written with AI support so please do check before running. If it is OK, I will publish to github.

node check-supply-chain.mjs <package-name> [start-folder|"global"]
node check-supply-chain.mjs axios
node check-supply-chain.mjs axios /path/to/projects
node check-supply-chain.mjs axios global
#!/usr/bin/env node
/** check-supply-chain.mjs - Supply-chain risk checker for npm package dependencies.
 *
 * Searches a directory tree for npm project roots and reports which packages
 * depend on a specified npm package, along with what versions are installed.
 * Designed to quickly assess potential supply-chain compromises (e.g. Axios-style
 * attacks) across all local projects.
 *
 * Usage:
 *   node check-supply-chain.mjs <package-name> [start-folder|"global"]
 *
 * Arguments:
 *   package-name   The npm package name to search for (e.g. axios)
 *   start-folder   Directory to search from (default: current working directory)
 *                  Use "global" to check globally installed packages instead.
 *
 * @example
 *   node bin/check-supply-chain.mjs axios
 *   node bin/check-supply-chain.mjs axios /home/user/projects
 *   node bin/check-supply-chain.mjs axios global
 *
 * @module check-supply-chain
 */

import { readdir, readFile, stat } from 'node:fs/promises'
import { join, resolve, relative } from 'node:path'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import process from 'node:process'

const execAsync = promisify(exec)

// โ”€โ”€ Argument parsing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const [,, targetPkg, startArg = '.'] = process.argv

/** Validate the package name against npm's allowed character set.
 * Scoped: @scope/name โ€” Unscoped: name
 * Only allows: lowercase letters, digits, hyphens, underscores, dots, @, /
 * This prevents shell injection when shell:true is used on Windows.
 * @type {RegExp}
 */
const NPM_PKG_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i

if (!targetPkg || targetPkg === '--help' || targetPkg === '-h') {
    console.log(`
Supply-Chain Dependency Checker
================================
Usage:  check-supply-chain <package-name> [start-folder|"global"]

Arguments:
  package-name   The npm package name to search for (e.g. axios)
  start-folder   Directory to search from (default: current directory)
                 Use "global" to check globally installed packages

Examples:
  node check-supply-chain.mjs axios
  node check-supply-chain.mjs axios /home/user/projects
  node check-supply-chain.mjs axios global
`)
    process.exit(targetPkg ? 0 : 1)
}

if (!NPM_PKG_RE.test(targetPkg)) {
    console.error(`\n  ERROR: "${targetPkg}" is not a valid npm package name.`)
    console.error('  Package names may only contain: letters, digits, hyphens, underscores, dots, and an optional @scope/ prefix.')
    process.exit(1)
}

const isGlobal = startArg.toLowerCase() === 'global'
const startFolder = isGlobal ? null : resolve(startArg)

// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/**
 * Find all npm project roots within a directory tree. A project root is a
 * directory that contains both a `package.json` file and a `node_modules`
 * directory. Skips `node_modules` directories and hidden directories (those
 * starting with `.`) to avoid false positives and infinite recursion.
 *
 * @param {string} dir - Absolute path of the directory to search recursively
 * @returns {Promise<string[]>} Absolute paths of all project roots found
 */
async function findProjectRoots(dir) {
    const roots = []
    let entries

    try {
        entries = await readdir(dir, { withFileTypes: true })
    } catch {
        return roots
    }

    const hasPackageJson = entries.some(e => e.isFile() && e.name === 'package.json')
    const hasNodeModules = entries.some(e => e.isDirectory() && e.name === 'node_modules')

    if (hasPackageJson && hasNodeModules) {
        roots.push(dir)
        // Still recurse: mono-repos can have nested project roots with their own node_modules
    }

    for (const entry of entries) {
        if (!entry.isDirectory()) continue
        // Skip node_modules and hidden directories (e.g. .git, .cache, .npm)
        if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue

        const childRoots = await findProjectRoots(join(dir, entry.name))
        roots.push(...childRoots)
    }

    return roots
}

/**
 * Run `npm ls <targetPkg> --all --json` via the shell and return the parsed
 * JSON output. Only used for the --global mode (local projects use the
 * lock-file parser instead, which is faster and more reliable).
 *
 * Uses `exec()` (shell-based) so that npm works on Windows, where it is a
 * batch file (.cmd) that cannot be launched with execFile without a shell.
 * Safe because `targetPkg` is validated against npm's character set before
 * reaching here โ€” none of those characters are shell metacharacters.
 *
 * @param {string|null} cwd - Working directory; null when using --global
 * @param {boolean} [global=false] - Whether to pass the --global flag
 * @returns {Promise<object|null>} Parsed npm ls JSON tree, or null on failure
 */
async function runNpmLs(cwd, global = false) {
    const globalFlag = global ? ' --global' : ''
    const cmd = `npm ls "${targetPkg}" --all --json${globalFlag}`

    try {
        const { stdout } = await execAsync(cmd, {
            cwd: cwd ?? undefined,
            env: process.env,
            timeout: 60_000,
            maxBuffer: 50 * 1024 * 1024,
        })
        return JSON.parse(stdout)
    } catch (err) {
        // npm ls exits 1 for missing peer deps but stdout is still valid JSON
        if (err.stdout) {
            try { return JSON.parse(err.stdout) } catch { /* fall through */ }
        }
        return null
    }
}

/**
 * @typedef {object} LockFinding
 * @property {string}   version    - Installed version of targetPkg
 * @property {string}   installedAt - node_modules path key from the lock file
 * @property {string[]} dependents  - Package names that declare targetPkg as a dependency
 * @property {boolean}  isDirect    - True when the project root itself declares the dep
 */

/**
 * Analyse a v2/v3 package-lock.json (npm 7+) flat packages map for all
 * installed instances of `targetPkg` and collect the packages that depend
 * on each instance.
 *
 * Lock-file format (v2/v3):
 *   packages[""] = project root
 *   packages["node_modules/foo"] = installed package foo
 *   packages["node_modules/bar/node_modules/foo"] = nested install of foo inside bar
 *
 * @param {object} lock - Parsed package-lock.json object
 * @param {object|null} [rootPkgJson=null] - Parsed package.json for the project root.
 *   When provided (e.g. when using node_modules/.package-lock.json which lacks
 *   the root `""` entry) it is used to detect direct dependencies.
 * @returns {LockFinding[]} One entry per installed instance of targetPkg
 */
function analyseLockFile(lock, rootPkgJson = null) {
    const pkgs = lock.packages
    if (!pkgs || typeof pkgs !== 'object') return []

    // Suffix patterns that identify an installed instance of targetPkg.
    // Both slash styles handled for cross-platform lock files.
    const suffix = `node_modules/${targetPkg}`

    /** @type {Map<string, LockFinding>} keyed by the lock-file path */
    const instances = new Map()

    for (const [path, data] of Object.entries(pkgs)) {
        if (path === suffix || path.endsWith(`/${suffix}`)) {
            instances.set(path, {
                version: data.version ?? 'unknown',
                installedAt: path,
                dependents: [],
                isDirect: false,
            })
        }
    }

    if (instances.size === 0) return []

    // Walk all packages to find which ones declare targetPkg in their deps
    for (const [path, data] of Object.entries(pkgs)) {
        const allDeps = Object.assign(
            {},
            data.dependencies,
            data.devDependencies,
            data.optionalDependencies,
            data.peerDependencies,
        )
        if (!(targetPkg in allDeps)) continue

        const isRoot = path === ''
        // Derive the declaring package's display name
        const declarer = isRoot
            ? '(project root / direct dependency)'
            : path.replace(/^.*node_modules\//, '')

        // Attribute the dependent to the closest applicable instance.
        // Instances nested inside the same parent path take priority.
        let bestMatch = null
        let bestLen = -1
        for (const [instPath] of instances) {
            const parentOfInst = instPath.slice(0, instPath.length - suffix.length - 1)
            const matchBase = path === '' ? '' : path
            if (matchBase.startsWith(parentOfInst) && parentOfInst.length > bestLen) {
                bestMatch = instPath
                bestLen = parentOfInst.length
            }
        }
        // Fall back to the top-level instance if no nested match
        if (!bestMatch) bestMatch = `node_modules/${targetPkg}`

        const inst = instances.get(bestMatch)
        if (inst) {
            if (isRoot) inst.isDirect = true
            if (!inst.dependents.includes(declarer)) inst.dependents.push(declarer)
        }
    }

    // Supplement: when using node_modules/.package-lock.json (no root "" entry),
    // cross-reference the project's package.json to detect direct dependencies.
    if (rootPkgJson) {
        const rootAllDeps = Object.assign(
            {},
            rootPkgJson.dependencies,
            rootPkgJson.devDependencies,
            rootPkgJson.optionalDependencies,
            rootPkgJson.peerDependencies,
        )
        if (targetPkg in rootAllDeps) {
            const topKey = `node_modules/${targetPkg}`
            const topInst = instances.get(topKey)
            if (topInst && !topInst.isDirect) {
                topInst.isDirect = true
                const label = '(project root / direct dependency)'
                if (!topInst.dependents.includes(label)) topInst.dependents.unshift(label)
            }
        }
    }

    return [...instances.values()]
}

/**
 * @typedef {object} DependencyFinding
 * @property {string[]} chain  - Package names forming the dependency chain leading to targetPkg
 * @property {string}   version    - Installed version of targetPkg at this node
 * @property {boolean}  deduped    - True when npm has hoisted/deduped this instance
 * @property {boolean}  overridden - True when an npm override has altered this instance
 */

/**
 * Recursively walk an npm ls JSON dependency tree and collect every occurrence
 * of `targetPkg`, recording the dependency chain that leads to each one.
 * Used only for the global npm ls output (which has no lock file).
 *
 * @param {object}   node      - Current tree node from the npm ls JSON output
 * @param {string[]} [chain=[]] - Package names of ancestors leading to this node
 * @returns {DependencyFinding[]} All occurrences of targetPkg under this node
 */
function collectFindingsFromTree(node, chain = []) {
    if (!node.dependencies) return []
    const results = []

    for (const [name, data] of Object.entries(node.dependencies)) {
        if (name === targetPkg) {
            results.push({
                chain,
                version: data.version ?? 'unknown',
                deduped: data.deduped === true,
                overridden: data.overridden === true,
            })
        }
        results.push(...collectFindingsFromTree(data, [...chain, name]))
    }

    return results
}

// โ”€โ”€ Formatting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const W = 70 // display width
const RULE = 'โ”€'.repeat(W)
const DOUBLE_RULE = 'โ•'.repeat(W)

/**
 * Attempt to read and parse a lock file from the given project root.
 * Tries `package-lock.json`, `npm-shrinkwrap.json`, and finally the hidden
 * `node_modules/.package-lock.json` that npm v7+ always writes.
 *
 * @param {string} root - Absolute path to the project root directory
 * @returns {Promise<object|null>} Parsed lock file object, or null if unavailable
 */
async function readLockFile(root) {
    for (const name of ['package-lock.json', 'npm-shrinkwrap.json', 'node_modules/.package-lock.json']) {
        try {
            return JSON.parse(await readFile(join(root, name), 'utf8'))
        } catch { /* try next */ }
    }
    return null
}

/**
 * Read and parse the project's package.json.
 *
 * @param {string} root - Absolute path to the project root directory
 * @returns {Promise<object|null>} Parsed package.json, or null if unreadable
 */
async function readPackageJson(root) {
    try {
        return JSON.parse(await readFile(join(root, 'package.json'), 'utf8'))
    } catch {
        return null
    }
}

/**
 * Print a lock-file based report for a single local project.
 *
 * @param {string}        projectName - Display name of the project
 * @param {string}        projectPath - Absolute filesystem path of the project root
 * @param {string}        relPath     - Path relative to startFolder
 * @param {LockFinding[]} findings    - Pre-computed findings from analyseLockFile()
 */
function printLockReport(projectName, projectPath, relPath, findings) {
    console.log(`\n${RULE}`)
    console.log(`Project : ${projectName}`)
    if (relPath) console.log(`Rel path: ${relPath}`)
    console.log(`Path    : ${projectPath}`)
    console.log(RULE)

    if (findings.length === 0) {
        console.log(`  (not found โ€” "${targetPkg}" is not in this project's lock file)`)
        return
    }

    const plural = findings.length === 1 ? 'instance' : 'instances'
    console.log(`  *** FOUND: ${findings.length} installed ${plural} of "${targetPkg}" ***\n`)

    for (const f of findings) {
        const location = f.installedAt === `node_modules/${targetPkg}`
            ? 'top-level (hoisted)'
            : `nested โ€” ${f.installedAt}`
        console.log(`  Installed version : ${f.version}`)
        console.log(`  Location          : ${location}`)
        if (f.isDirect) {
            console.log(`  Direct dependency : yes`)
        }
        if (f.dependents.length > 0) {
            console.log(`  Depended on by    :`)
            for (const d of f.dependents) {
                console.log(`    โ€ข ${d}`)
            }
        } else {
            console.log(`  Depended on by    : (no dependents recorded in lock file)`)
        }
        console.log()
    }
}

/**
 * Print a global npm ls tree-based report.
 *
 * @param {DependencyFinding[]} findings - From collectFindingsFromTree()
 */
function printGlobalReport(findings) {
    console.log(`\n${RULE}`)
    console.log(`Scope : Global npm packages`)
    console.log(RULE)

    if (findings.length === 0) {
        console.log(`  (not found โ€” "${targetPkg}" is not in the global npm dependency tree)`)
        return
    }

    /** @type {Map<string, DependencyFinding[]>} */
    const byVersion = new Map()
    for (const f of findings) {
        const list = byVersion.get(f.version) ?? []
        list.push(f)
        byVersion.set(f.version, list)
    }

    const plural = findings.length === 1 ? 'place' : 'places'
    console.log(`  *** FOUND: "${targetPkg}" appears in ${findings.length} ${plural} ***\n`)

    for (const [version, instances] of byVersion) {
        console.log(`  Installed version: ${version}`)
        for (const inst of instances) {
            const depPath = inst.chain.length > 0
                ? inst.chain.join(' โ†’ ') + ` โ†’ ${targetPkg}`
                : `${targetPkg}  (direct global dependency)`
            const flags = [
                inst.deduped && '(deduped)',
                inst.overridden && '(overridden)',
            ].filter(Boolean)
            const flagStr = flags.length > 0 ? `  ${flags.join(' ')}` : ''
            console.log(`    โ€ข ${depPath}${flagStr}`)
        }
        console.log()
    }
}

// โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

console.log(`\n${DOUBLE_RULE}`)
console.log('Supply-Chain Dependency Checker')
console.log(DOUBLE_RULE)
console.log(`Target  : ${targetPkg}`)
console.log(`Scope   : ${isGlobal ? 'Global npm packages' : startFolder}`)
console.log(DOUBLE_RULE)

if (isGlobal) {
    // โ”€โ”€ Global mode: use npm ls (no lock file for global installs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    console.log('\nQuerying global npm packages (this may take a moment)...')

    const tree = await runNpmLs(null, true)

    if (!tree) {
        console.error('\n  ERROR: Failed to run "npm ls --global". Is npm available in PATH?')
        process.exit(1)
    }

    const findings = collectFindingsFromTree(tree)
    printGlobalReport(findings)

    console.log(`\n${DOUBLE_RULE}`)
    if (findings.length > 0) {
        console.log(`SUMMARY: "${targetPkg}" IS present in global packages โ€” review versions above.`)
        console.log(`\n  *** Check the package's changelog and known CVEs immediately. ***`)
    } else {
        console.log(`SUMMARY: "${targetPkg}" was NOT found in global packages.`)
    }
} else {
    // โ”€โ”€ Local folder mode: read lock files directly (fast, no subprocess) โ”€โ”€โ”€โ”€โ”€

    try {
        const s = await stat(startFolder)
        if (!s.isDirectory()) {
            console.error(`\n  ERROR: "${startFolder}" is not a directory.`)
            process.exit(1)
        }
    } catch {
        console.error(`\n  ERROR: Start folder "${startFolder}" does not exist or is not accessible.`)
        process.exit(1)
    }

    console.log('\nSearching for npm project roots (directories with package.json + node_modules)...')
    const roots = await findProjectRoots(startFolder)

    if (roots.length === 0) {
        console.log('\n  No npm project roots found.')
        console.log('  (A project root requires both a package.json and a node_modules directory)')
        process.exit(0)
    }

    console.log(`Found ${roots.length} project root(s). Analysing lock files...\n`)

    let foundCount = 0
    let noLockCount = 0

    /** @type {Array<{name: string, path: string, relPath: string, findings: LockFinding[], hasLock: boolean}>} */
    const allResults = []

    for (const root of roots) {
        const relPath = relative(startFolder, root) || '.'
        const pkgJson = await readPackageJson(root)
        const projectName = pkgJson?.name ?? relPath

        process.stdout.write(`  Checking ${projectName} ... `)

        const lock = await readLockFile(root)
        if (!lock) {
            console.log('SKIP (no lock file found)')
            noLockCount++
            allResults.push({ name: projectName, path: root, relPath, findings: [], hasLock: false })
            continue
        }

        const lockVer = lock.lockfileVersion ?? 1
        let findings = []

        if (lockVer >= 2 && lock.packages) {
            // v2/v3 lock file โ€” flat packages map (npm 7+)
            // Pass pkgJson so the hidden .package-lock.json case can detect direct deps
            findings = analyseLockFile(lock, pkgJson)
        } else {
            // v1 lock file โ€” fall back to npm ls subprocess
            process.stdout.write('(v1 lock, using npm ls) ')
            const npmTree = await runNpmLs(root)
            if (npmTree) findings = collectFindingsFromTree(npmTree).map(f => ({
                version: f.version,
                installedAt: `node_modules/${targetPkg}`,
                dependents: f.chain.length > 0 ? [f.chain.join(' โ†’ ')] : ['(project root)'],
                isDirect: f.chain.length === 0,
            }))
        }

        if (findings.length > 0) {
            foundCount++
            console.log(`FOUND (${findings.length} instance${findings.length !== 1 ? 's' : ''})`)
        } else {
            console.log('not found')
        }

        allResults.push({ name: projectName, path: root, relPath, findings, hasLock: true })
    }

    // Detailed reports โ€” only print header for not-found projects, full detail for found
    for (const r of allResults) {
        if (r.hasLock || r.findings.length > 0) {
            printLockReport(r.name, r.path, r.relPath, r.findings)
        }
    }

    const checked = allResults.filter(r => r.hasLock).length
    console.log(`\n${DOUBLE_RULE}`)
    console.log(`SUMMARY`)
    console.log(RULE)
    console.log(`  Projects scanned      : ${checked}`)
    console.log(`  Skipped (no lock file): ${noLockCount}`)
    console.log(`  "${targetPkg}" found in     : ${foundCount} project(s)`)
    if (foundCount > 0) {
        console.log(`\n  *** Review the dependency details above for version risks.    ***`)
        console.log(`  *** Check the package's changelog and known CVEs immediately. ***`)
    } else {
        console.log(`\n  No dependency on "${targetPkg}" detected in any scanned project.`)
    }
}

console.log(`${DOUBLE_RULE}\n`)

Run against node-red's root folder:

โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Supply-Chain Dependency Checker
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Target  : axios
Scope   : D:\src\nrnext
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Searching for npm project roots (directories with package.json + node_modules)...
Found 2 project root(s). Analysing lock files...

  Checking node-red-master ... FOUND (1 instance)
  Checking node-red-userdir ... not found

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Project : node-red-master
Rel path: .
Path    : D:\src\nrnext
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  *** FOUND: 1 installed instance of "axios" ***

  Installed version : 1.13.6
  Location          : top-level (hoisted)
  Depended on by    :
    โ€ข node-red-admin

Just run against my dev installation and it found the same version of Axios depended on by Dashboard 2 as well.

And published to npm

https://www.npmjs.com/package/npm-supply-chain-check

You don't need to install it, you can run it with npx:

cd ~/.node-red
npx npm-supply-chain-check axios
npx npm-supply-chain-check axios global
1 Like

Ah, just realised that I also have node-red-dev installed globally. Probably a really bad idea since it is so far out of date. It has v0.21.4 of Axios as a dependency. Not compromised thankfully, but still massively out of date which presents its own risks.

I have always felt uneasy about npx and the irony around it encouraging people to blindly run code from the internet with no chance to examine it.... :wink:

3 Likes

Indeed :smiley:

Of course, which is why I certainly encourage people to check the script before you run anything.

It can, of course, also be installed and run as any other npm package.

I would say that I have locked down the github source to prevent any unwanted changes and there are NO DEPENDENCIES in the package other than node.js itself. My npm account is also well protected.

@TotallyInformation : thanks, your script worked fine, I trusted it fully, in the RPi5 I checked, I had versions 0.21.4, 0.22.0, 1.13.4 and 1.12.2

Since a lot has been discussed about security in NR lately (e.g. NRG Sentinel threads...) I wonder if that tool would help to protect against these kind of threats

1 Like

It is more of a manual check to be honest. Timing is so critical and there are many other factors to consider sometimes. Even finding a consistent source of package names and versions to check against can be hard.

If someone can come up with a reliable process, I'll happily have a go at coding it though.