Decoding CVEs: A practical guide to assessing and mitigating security risks
Summary
Let's explore the world of Common Vulnerabilities and Exposures (CVEs) with step-by-step examples of evaluating if a CVE impacts your project and pragmatic strategies for effective mitigation. This guide will empower you to tackle security vulnerabilities head-on. Don't let CVE warnings go unnoticed — learn how to address them confidently and efficiently.
Content
As developers, we're constantly juggling multiple projects, prioritizing bug fixes and feature enhancements. Yet, amidst this hectic pace, we often stumble upon the unsettling reality that our projects may be susceptible to Common Vulnerabilities and Exposures (CVEs). With each passing day, the number of reported CVEs continues to rise, presenting a daunting challenge in maintaining the security of our projects. But fear not, for I'm here to share practical strategies to tackle this issue head-on. In this review, I'll outline several approaches to seamlessly integrate CVE review into your development workflow, empowering you to safeguard your projects with confidence
If we don’t have a clear strategy to mitigate the CVE, we will feel overwhelmed very easily by the number of CVE that are discovered every day, especially in the Node.js ecosystem, where it is very common to have a lot of dependencies in our applications. Ulises Gascón. Node.js for Beginners (Chapter 15. Securing Web Applications)
How to review a CVE?
Let's take CVE-2020-8203 as an example. This prototype of a pollution vulnerability affects the popular library Lodash.
Depending on the tools we use to get notice on the vulnerabilities in our project, we might notice different reports that echo more or less the same information:
As you can see, both images share similar information (mostly metadata) about the vulnerability. However, each description provides different information that can enrich our analysis.
Review severity
First, we can use severity to help us prioritize the work. This is based on a scale from one to ten, using the Common Vulnerability Scoring System (CVSS) to determine this number. This scoring will also break down and can give us a lot of context. The idea is to prioritize the patching based on criticality if we have multiple vulnerabilities to review.
Note that based on the information provider you might get different results. In the image below we can see that Snyk assigns 8.2, while Red Hat and NVD assign 7.4.
It doesn’t matter which scoring system you follow, as long as you use the same one all the time. Overall critical or high CVEs are important to review, while lower severity CVEs might give us more time.
Just to be very clear, once a CVE is made public anyone in the world is aware of this, so bad actors can easily integrate these critical CVEs in their arsenals and start using them to exploit systems if they find the way to do it.
If you're not familiar with how fast the bad actors can act, checkout this honeypot report.
Get more information
Our next step is to get as much information as possible of this vulnerability and the potential exploitation. Sometimes you can get a lot of information by reviewing the links included in the references section in the CVE official report page.
In this case, we can even read the original report in HackerOne and the discussion between the reporter and the maintainers. This can provide us with a lot of information. In this case, we can find good information in the Snyk report and GitHub report.
Clarify the attacking surface
From the previous step, we should be clear on which versions of the library are affected (>=4.1.0 <4.17.20
) and what methods are in scope for this vulnerability (pick
, set
, setWith
, update
, updateWith
, and zipObjectDeep
) — keeping in mind that we know we’re facing a prototype pollution vulnerability.
We can do a quick check in the repository and evaluate if we are exposed or not. If we’re not using these methods with these specific versions, we can confirm that our code is not affected and this is a false positive in our case.
This evaluation can be more complex than expected. In the JavaScript ecosystem, it is very common to have devDependencies in development tools that do not have a real impact in the production environment — for example, linters, testing frameworks, certain utils, etc. This depends a lot on the specific nature of the project. We can use PM2 locally to simulate a productive environment but use containers and kubernetes to deploy our projects in the productive environment. This will limit a lot the attack vectors for CVEs related to PM2 as most in most of the cases we are running this in a "safe environment" that we have a full control on, but keep in mind that even if the package is not used in production it can have negative effects in our development environment if compromised, for example eslint was compromised in 2018.
If we don’t have a clear argument against this vulnerability in our project, it is safe to say that we can consider ourselves affected by this potential vulnerability and might need to provide a valid mitigation.
Aside from the information provided in the description, a good way to understand more about the attack technique is to check the Common Weakness Enumeration (CWEs) mentioned in the CVE.
In this case the CWE mentioned was CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution'). This helps us a lot to find similar CVEs and to understand better how this can be exploited.
In simple words, the CWE is a list of possible weaknesses, and the CVE is a list of vulnerabilities that have been discovered in the wild. Ulises Gascón. Node.js for Beginners
Is there POC that can be exploited?
Not all the CVEs included a proof of concept (POC) of the vulnerability reported that can be easily exploited, but in this case we have a clear one:
const _ = require('lodash');
_.zipObjectDeep(['__proto__.z'],[123]);
console.log(z); // 123
Aside from helping us to better understand the issue, this can help us test our code to make a positive confirmation of the vulnerability. Let's assume that we have the following code in our application:
const _ = require('lodash');
const normalizeData = (users, ages) => {
return _.zipObjectDeep(users,ages);
}
module.exports = { normalizeData }
We can easily assume it’s vulnerable to the prototype pollution attack, but we can also build a test to ensure that we have the mechanisms in place to prevent these vulnerabilities from happening in the future. Sometimes the libraries introduce regressions that contains vulnerabilities, which we can prevent by adding a simple test as follows:
const { normalizeData } = require('../utils');
describe('normalizeData behaviour', () => {
it('should return the data structured properly', () => {
const users = ['John', 'Doe'];
const ages = [30, 40];
const result = normalizeData(users, ages);
expect(result).toEqual({ John: 30, Doe: 40 });
});
it('should not be affected by a prototype pollution (CVE-2020-8203)', () => {
normalizeData(['__proto__.z'], [123]);
expect(global.z).toBe(undefined);
});
});
Obviously, this test will fail as currently we are vulnerable to this prototype pollution and we didn't make any change in our project to mitigate this.
figure: Using Node.js@21 and lodash@4.17.0
Now, Let's see how we can mitigate this vulnerability.
Strategies to mitigate a CVE?
There are several strategies to mitigate a CVE, we will explore some of them in order of effort investment.
Upgrade or downgrade versions
The most recommended way to mitigate a CVE is to upgrade the version of the library that is affected. In this case, we can upgrade to lodash@4.17.20
or higher. This is often the best method because library maintainers will provide a fix for the vulnerability that was also tested with the reporter, so we can be sure that this patch is working.
So, if we apply this patch (npm i lodash@4.17.20
) we can re-run the test that we built before and we can ensure that the vulnerability is mitigated.
Figure: Using Node.js@21 and lodash@4.17.21
While this seems an easy task, it can become very complex. We might need to upgrade or downgrade other dependencies that are not compatible with the new version of the library, or the newer version of the library may contain other changes that are not compatible with our code.
Other times, the CVE was made public before the patch was released, so we need to explore other strategies to mitigate the vulnerability while waiting for the final patch.
Migrate
If the library is no longer maintained, or the maintainers are not providing a patch for the vulnerability, we might need to migrate to another library that provides the same or similar functionality. While it’s a large effort, it can be a good investment if this new library is more secure and maintained than the previous one.
So we can be sure that in the future we will have fewer CVEs to review and we can manage the upgrades more easily.
Manual patching (DIY)
If we can't upgrade the library or migrate to another library, we might need to patch it ourselves. This is also a valid strategy when the CVE is made public before the patch is released, and we need to mitigate the vulnerability as soon as possible while waiting for the official patch.
Any manual path requires a good understanding of the library and the vulnerability. Here we can rely on the POC that was provided in the CVE report to build our own patch and we can use the test that we build to ensure that the patch is working. Let's see how we can build a patch for this vulnerability:
const _ = require('lodash');
const normalizeData = (users, ages) => {
const safeUsers = users.filter(user => !/__proto__|prototype|constructor/.test(user));
return _.zipObjectDeep(safeUsers, ages);
}
module.exports = { normalizeData }
This was a simple patch that illustrates how to mitigate the vulnerability, but for better compliance please review the official patch as our test case was very basic. We can re-run the test now and we can ensure that the vulnerability is mitigated.
Figure: Using Node.js@21 and lodash@4.17.0 and the patch
Strategies to document decisions made around CVEs
When we are working in a team, it is important to document the decisions that we made around the CVEs, this will help us to keep track of the vulnerabilities that we mitigated and the ones that we are still pending to mitigate. This will help us to prioritize the work and to ensure that we are not missing any vulnerability.
We can use a simple table to document the CVEs that we are reviewing, the severity, the status, the mitigation strategy and the date that we mitigated the vulnerability. This will help us to keep track of the vulnerabilities that we mitigated and the ones that we are still pending to mitigate.
CVE | Severity | Status | Mitigation Strategy | Date |
CVE-2020-8203 | 8.2 | Mitigated | Upgrade to lodash@4.17.21 | 2024-04-15 |
This table can be updated every time that we review a CVE, and shared with the team to ensure that everyone is aware of the vulnerabilities that we are facing and the ones that we mitigated.
Even better, you can include this table with the project’s documentation, so it is included as part of the code’s distribution process.
If you are using a tool to review the CVEs, it can be hard to clarify the status of the CVE that has been identified as a false positive or the ones that we did a manual patch for. For example if you are using Snyk you can mark the CVE as ignored.
In our case, if you used the manual patch you can mark the CVE as ignored in the Snyk report as follows snyk ignore --id=SNYK-JS-LODASH-567746 --reason="manual patch in place with tests"
.
This will generate the following content in the .snyk
file:
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JS-LODASH-567746:
- '*':
reason: manual patch in place with tests
expires: 2024-05-15T17:27:20.339Z
created: 2024-04-15T17:27:20.340Z
patch: {}
Overall, the process requires some manual work, and working with a team is highly recommended to avoid bias and human errors as much as possible.
Checklist
So, to summarize the steps that we need to follow to review a CVE:
Review the severity of the CVE
Get more information about the attack vector
Clarify the attacking surface
Check if there is a POC that can be exploited
Evaluate the impact in your project
Mitigate the vulnerability if needed
Document the decisions made around the CVEs
Share the conclusions with your team
Additional resources
If you want to explore more topics around security in Node.js, you can check my book Node.js for Beginners where I cover a lot of topics around security in Node.js.