Security risks may arise in your applications not just from your own code but also from third-party dependencies. Learn how you can protect your web applications from vulnerabilities that come from third-party assets.
Third-Party Assets Risks
Building web applications requires special attention to security issues since they are publicly accessible. As a developer, you should make sure that your applications are reasonably protected from the most common security concerns.
However, your effort to build secure web applications may be nullified by vulnerabilities that may exist in third-party assets such as library packages, JavaScript scripts, or CSS files. Those external resources may contain vulnerabilities that affect your application. In other words, a vulnerability in a third-party asset becomes a vulnerability in your application.
What are the risks you may face with third-party vulnerabilities? They are basically the same risks you can have with vulnerabilities in your own code:
- exposure to Cross-Site Scripting (XSS)
- being subject to Cross-Site Request Forgery (CSRF) attacks
- predisposition to clickjacking tricks
- injection flaws and other common attacks
However, you may have additional risks due to potential deliberate malicious code in any of your project's dependencies. For example, this malicious code may expose you to:
- loss of confidential and personal data
- unauthorized access to systems and other applications
- downtime of system infrastructure
A few notorious cases, like the event-stream
package threat or the Equifax incident, are emblematic examples of what can happen if you neglect third-party packages security.
Let's discover what you can do to mitigate such risks.
"A vulnerability in a third-party asset becomes a vulnerability in your application."
Tweet This
Best Practices to Mitigate Risks
To mitigate the risk of introducing vulnerabilities in your web application due to third-party assets, you should set up a structured process in your development flow. This process is basically composed by four steps:
- Assets inventory
- Dependencies analysis
- Risk assessment
- Risk mitigation
Let's take a look at each of these steps.
Assets inventory
The first step in this process is to know what dependencies your web application is using. This may seem a naive assertion, but, honestly, do you actually know how many dependencies your application uses?
Think of the node_modules
folder of your Node.js project or the global packages folder for .NET Core solutions or the equivalent for other programming languages and development environments. They usually contain a lot of packages. Maybe you installed a few of them, but most likely, they depend on other packages. So, you end up having a significant amount of code you have not written in your project. And the more uncontrolled code you have in your codebase, the more likely you introduce vulnerabilities into your application.
Also, don't forget to consider even scripts or stylesheets you include in your client applications, regardless of whether you include a bundled copy of them or link to a Content Delivery Network (CDN). Think of jQuery and Bootstrap assets or the Google Analytics tag, for example.
Knowing exactly which dependencies your project uses is fundamental to know the associated risks and to mitigate them. This step may also give you the opportunity to think about the real need for each third-party dependency.
Dependencies analysis
Once you list the dependencies used in your project, you should inspect them to search for vulnerabilities.
If the dependency comes from an open-source project, you can check its code. Otherwise, you can consult the vendor's security bulletins or check out specialized databases such as the National Vulnerability Database.
Of course, manually checking all your dependencies is not feasible. You should use tools that automate this task for you. For example, if you use GitHub as your code repository service provider, you should regularly receive security vulnerabilities alerts that may affect your projects' dependencies. They also provide automated pull requests via Dependabot.
Even npm provides you with the ability to check for security vulnerabilities in your Node.js projects. You can launch this inspection by typing npm audit
in a terminal window. You can also automatically update your npm dependency by running npm audit fix
.
Alternatively, you can use OWASP Dependency-Check, a tool from OWASP that integrates with other code management systems such as Maven, Gradle, Jenkins, and others. Or you can use the OWASP Dependency-Track platform to identify security risks with a structured and advanced approach.
Whatever tool you use, the result of this analysis step is a list of the vulnerabilities that affect your dependencies and their severity.
Risk assessment
Of course, your hope is to get an empty vulnerability list for your dependencies. However, the chances are that you detect a few security risks affecting your application. In this case, you have to make some decisions.
Usually, your decisions depend on your answers to the following questions:
- How severe is that vulnerability?
- Is there any available fix?
- What is the effort to fix the vulnerability?
- Is this vulnerability relevant to your application?
You may think that if a new version of your dependency fixes that vulnerability, you have to install that update. However, updating a dependency is not always a smooth task. You need to be sure that that new version doesn't break your application or other dependencies. Also, you need to make sure that the new version is not introducing new security issues. You need to balance the effort of upgrading and the benefits it brings.
Maybe you can find vulnerabilities that actually don't affect your application, so you can postpone the dependency upgrade to a later time. In other words, in some circumstances, you can decide to accept the risk represented by the security issue you discovered in a dependency.
So, this step of the mitigation process is basically a thoughtful reasoning about the risks and benefits of taking an action over the issues. To learn more about this decisional process, take a look at the OWASP Vulnerable Dependency Management Cheat Sheet.
Risk mitigation
Now you have a clear picture of the situation. You may have vulnerabilities that can be temporarily ignored, but you may have issues that need to be fixed.
If a fix for a vulnerability exists and the upgrade is straightforward for your application, you should apply it. In case there's no fix for a vulnerability affecting one of your dependencies, you have two choices:
- Collaborating with the author/vendor of the dependency to solve the vulnerability.
- Find a workaround to prevent the vulnerability exploitation.
At the end of this process, you should have addressed the most dangerous vulnerabilities that could affect your application.
" By using third-party dependencies, you are taking responsibility for code you did not write."
Tweet This
Keeping Risk Mitigation Under Control
Even if you have been able to ensure an acceptable level of risk with your dependencies, you can't relax yet. Your codebase is a live environment. Even if you don't change your code, your third-party assets may change without you realizing it. Just a simple rebuild can add uncontrolled code to your project. The same situation may happen without any build process when you use a third-party script or CDN.
You may have different scenarios depending on the type of project and its configuration. Let's see how to deal with those issues.
Locking Server-Side Assets
Most modern development environments rely on centralized remote registries for dependency management, such as, for example, npm, NuGet, Maven, Packagist, and others. Even a simple rebuild of your project may trigger the download and installation of dependency updates without you realizing it.
Of course, it depends on your package manager configuration. Let's take npm as an example. It is the standard Node.js package manager, and it relies on the package.json
configuration file to track a Node.js project's dependencies. This is an example of package.json
file:
{
"name": "my-app",
"version": "1.0.0",
"description": "This is my application",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.2",
"express": "^4.17.1"
}
}
It declares that the current project depends on axios version 0.19.2 and Express version 4.17.1. However, the caret (^
) before the version number tells the package manager that any compatible version with the current one can be installed. Following the semantic versioning convention, this means that the package manager is allowed to install any patch below version 0.20.0 for axios and any patch and minor fixes below version 5.0.0 for Express.
Say you found no security issues in the current versions of those packages. With this configuration, you may add an uncontrolled version of those dependencies in your project with the risk of introducing new vulnerabilities.
You have two ways to fix the dependency versions of your Node.js project:
- Specify the exact version of your dependencies in
package.json
by removing any range indicator. - Consider the
package-lock.json
file as an integral part of your project and commit it into your source code repository (see thepackage-lock.json
documentation for more information).
You may have the same problem with other package managers. For example, in .NET you can specify a reference in your project as in the following example:
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- ...other configuration items... -->
<PackageReference
Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication"
Version="3.2.*" />
<!-- ...other configuration items... -->
</Project>
In this case, NuGet will install the highest stable package version starting with 3.2. Again, you are not sure of the specific version you are running in your application. So, use a specific version for your dependency to make sure you are getting the package you trust. Check out the NuGet package versioning document to learn more.
Locking Client-Side Assets with Subresource Integrity
Hosting scripts and stylesheets on a CDN is a common practice. It helps to reduce bandwidth consumption and improve performance. However, that code is out of your control. The code maintainer may apply changes to it, or an attacker can replace it with malicious code without you realizing it.
Using third-party CDNs exposes your application to a potential security risk. How can you mitigate this risk?
The typical approach to mitigate it is to apply Subresource Integrity. This standard feature, supported by all recent browsers, allows you to block any third-party asset that changed since your latest security review. Let's take a quick look at how Subresource Integrity works.
Assume you have a reference to React in your HTML page as in the following:
<script src="https://unpkg.com/react@17.0.1/umd/react.production.min.js">
</script>
This reference uses the unpkg CDN to load a specific version of React. Indicating a specific version is a good practice to make sure you have the exact version of a library you already verified. In the unpkg CDN, if you just indicate a major version of a library, for example, react@17
, you will get the most recent update of that library. So, similarly to npm, you specify version 17 but will get version 17.0.1 and subsequent patches and minor updates.
However, even if you specify a given version of your library, you may not be sure if the code has not been tampered with, say, after an attack. You may also need to use libraries or scripts that don't provide any explicit version number.
The Subresource Integrity feature ensures that the third-party asset has not been changed since your latest security review. To enable that feature, you need to add two attributes to the script element seen above. The protected script would appear like the following:
<script src="https://unpkg.com/react@17.0.1/umd/react.production.min.js"
integrity="sha256-Ag0WTc8xFszCJo1qbkTKp3wBMdjpjogsZDAhnSge744="
crossorigin>
</script>
You added the integrity
and crossorigin
attributes to the <script>
tag.
The value of the integrity
attribute is the hash value of the third-party asset. It is represented by a string in the form algorithm-hash
. In the example above, the algorithm is sha256
, and the actual hash value is Ag0WTc8xFszCJo1qbkTKp3wBMdjpjogsZDAhnSge744=
.
The crossorigin
attribute enables the browser to requests the asset to the server and to check its integrity. Without crossorigin
the browser will load the asset independently on its integrity.
When the browser loads the asset from the CDN, it will compute the file's hash value. If the value is equal to the value specified in the integrity
attribute, the asset is loaded. Otherwise, the asset will be blocked, and an error will be thrown in the browser's console, as in the following picture:
The console shown above is from Chrome. In this specific browser, the error message also provides the expected correct value for the hash. So, you can leverage the Chrome console as a manual tool to compute the value for the integrity
attribute as a side effect of the integrity verification.
Consider that not all browsers generate an error message with the expected hash value.
Alternatively, you can use the SRI Hash Generator, an online tool that generates the whole script tag with the computed integrity
value.
If you prefer a command-line tool, you can use shasum
in a Linux-based environment. In this case, you need to download the asset on your machine to calculate the hash value. Assuming you downloaded the React library file on your machine, the command you need to run looks as follows:
shasum -b -a 256 react.production.min.js | awk '{ print $1 }' | xxd -r -p | base64
This will show the computed hash value on your screen.
You can also use openssl to generate the hash value. This is the command you have to run:
openssl dgst -sha256 -binary react.production.min.js | openssl base64 -A
So far, you have seen an example of protecting from an external script with Subresource Integrity. Additionally, the same mechanism applies also to CSS files. To protect your application from external stylesheets, you can use the same
integrity
andcrossorigin
attributes for the<link>
tag.
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.
Conclusion
Throughout this article, you learned that writing your code with security in mind may not be enough to make your application secure. You also need to pay attention to third-party assets. Once you add them to your application, they become a part of it, with any potential vulnerability they may have.
The article suggested the best practices you should follow to keep your third-party assets under control. First, you should know what your third-party assets are, which vulnerability they may be affected by, and which level of risk they bring to your application. Then, once you reach an acceptable security risk level, you need to freeze the current situation to avoid unwanted updates or tampering. You can do it by locking package versions on the server side and using Subresource Integrity on the client side.
To learn more about managing vulnerabilities of third-party dependencies, check out the OWASP Vulnerable Dependency Management Cheat Sheet.