Security in web development

Most of the frontend world and web development in general are built on trust. Developers and engineers trust the frameworks and libraries they use, and they trust the open-source community to build and maintain most of the tools they use daily.

The open-source community is incredible. It’s a space where developers from all around the world contribute their time and skills to create tools, libraries, and applications that others can use for free. This collaborative spirit has led to the development of some of the world’s most widely used and influential software.

However, this trust-based system is not without its flaws. While many open-source contributors are well-intentioned and diligent about security, there are cases where vulnerabilities slip through the cracks. Sometimes, these vulnerabilities are unintentional and result from oversight or lack of knowledge. Other times, malicious actors may intentionally introduce vulnerabilities into the codebase.

Press + to interact

These vulnerabilities can have far-reaching consequences. If a hacker exploits a vulnerability in a popular package, they can potentially gain unauthorized access to thousands of applications and the data of millions of users.

If we have just started dipping our toes into the wonderful ocean that is making websites, apps, or software for a living, or even if we’re an experienced engineer who has seen it all during our career, we might be surprised to learn about the dangers that we can inflict on our users.

Not intentionally, of course, but by not knowing the security risks our code can cause, we can cause serious damage to our users, our codebase, our database, or our reputation. We have to ensure that every time we develop a solution to real-world problems, we keep security in our minds.

This course focuses on the security risks of front-end development and how to avoid them in our code. So, let’s start with the most common security vulnerability—the easiest to fix and avoid and the one that can cause the most damage.

Introduction to exploits

In the vast landscape of cybersecurity, one term we often encounter is “exploits.” At its core, an exploit represents a sequence of commands or a piece of software that takes advantage of a vulnerability within a system or application. These vulnerabilities can arise from software bugs, design flaws, or unintended misuse of legitimate features. By leveraging these weak points, exploits can allow unauthorized users to perform unintended actions, ranging from data extraction to taking complete control of a system.

As we delve deeper into the intricacies of digital security, it becomes evident that exploits play a pivotal role in cyber-attacks. They are tools in hackers’ arsenal, enabling them to breach defenses and achieve their malicious objectives. The impact of an exploit largely depends on the nature and severity of the vulnerability it targets. Some exploits may merely cause nuisances, while others can lead to significant data breaches or system downtimes.

Here is how an exploit attack can take place:

  1. An attacker finds a vulnerability in one of the libraries or packages used in our application, often listed in the package.json file. These vulnerabilities can range from minor bugs to critical security flaws.

  2. The attacker then creates malicious code or input to exploit this vulnerability. This is typically done so that the malicious input interacts with the compromised package when executed by our application.

  3. When our application runs and uses the vulnerable package, it inadvertently executes the attacker’s malicious code. This could occur during various operations, such as processing user inputs, fetching data, or any activity that involves the compromised package.

  4. As a result, executing this malicious code can lead to various security issues, including unauthorized access to user data, modification of application behavior, or even total control over the application, particularly if the package has extensive access within our application’s framework.

Code sample

In the following widget, we have implemented a sample React application that shows two buttons: one serializes a “Hello World” message, and the other deserializes it.

Please click the “Full screen” button at the top-right corner of the widget above to experience a full-screen environment that also displays the file structure of our application on the left side of the screen.

Serialisation explained

First and foremost, let’s examine the code above. It’s always exciting to dive into new code and learn its contents, so take a moment to familiarize yourself with each file and its contents.

The sample application demonstrates the processes of serialization and deserialization of JSON objects, a technique widely used in various applications. Serialization is the process of converting an object’s state into a format that can be stored or transmitted (like converting an object into a JSON string). This format enables the object’s state to be saved in a file or sent over a network. For instance, many contemporary applications use JSON format for configuration files. The application serializes its configuration—transforming complex data structures like settings or user preferences into a JSON string—and saves this string in a JSON file on disk.

On the other hand, deserialization is the reverse process. When the application restarts, it reads the JSON file and deserializes the JSON string back into objects and data structures. This process allows the application to restore its configuration or state from the saved data. By understanding serialization and deserialization, we gain insight into how applications maintain state, settings, and user data across sessions and even across different platforms.

The problem

While the application is working and the sample code runs without problems, we might not have noticed that this code has a big security vulnerability—not in the code itself but in the package we are using. It’s easy to overlook such issues, especially when everything seems to be running smoothly. However, it’s crucial to be aware that even seemingly perfect code can have hidden flaws that can be exploited by malicious users.

Let’s turn our attention to the highlighted line in package.json. The package we are using here, "serialize-javascript" , has a vulnerability that affects versions lower than 3.1. This is a common issue in the world of software development: third-party packages and libraries are continuously updated to address bugs and security vulnerabilities.

The serialize-javascript is an impressive open-source project pioneered and managed by Yahoo. What it does is unique: it serializes JavaScript to a superset of JSON, a task that involves handling expressions, dates, and functions.

The serialize-javascript NPM package is an extensively used library, but it had a flaw that attackers could exploit to execute remote code—a scenario referred to as Remote Code Execution (RCE).

Jordan Milne and Ryan Siebert discovered this security loophole and reported it to GitHub on May 20. After some confidential discussions, this issue was publicly revealed through the GitHub Advisory database.

Let’s delve a little deeper into this.

The vulnerability, which is tagged as CVE-2020-7660, provides a means for remote attackers to inject any arbitrary code via the deleteFunctions function present in index.js of the package. This vulnerability was found in serialize-javascript versions before 3.1.0.

Now, how does this work? Let’s look at a proof of concept to illustrate this.

Proof of concept

The serialize-javascript library is widely used, boasting over 16 million downloads and being a dependency in more than 840 projects. Having a vulnerability in a project of this size could impact millions of users and hundreds of companies.

This vulnerability in the serialize-javascript library is linked to how it handles the serialization of JavaScript objects. Serialization is the process of converting an object into a format (like a string) that can be stored or transmitted. Under normal circumstances, this process should be secure and predictable.

Let’s consider an object: {"foo": "example", "bar": "test"}. When serialized properly, it turns into a safe string, something like '{"foo":"example","bar":"test"}', which can be transmitted or stored without any issues. However, due to the vulnerability, the serialization process can be manipulated, leading to unexpected and potentially harmful outcomes.

For instance, suppose we have an object like {"foo": /1"/, "bar": "a"@R-<UID>-0@"}. Ideally, this should be serialized straightforwardly. However, the vulnerability allows this object to be altered during serialization. Here’s what it looks like when it’s affected by the vulnerability: {"foo": /1"/, "bar": "a/1"/}. This change might seem small, but it has a profound impact.

Consider another example. If an attacker has control over the input and knows how to manipulate the serialization process, they might craft an object like {"foo": "1;alert('Attack');", "bar": "safe"}. Due to the flaw, this could be serialized into a string that, when deserialized, doesn’t just represent data but also contains executable code, such as {"foo": "1;alert('Attack');", "bar": "safe"}. Here, the JavaScript function alert('Attack') could be executed, indicating that arbitrary code can be run through this process.

In practical terms, this means that if an attacker can manipulate the values of both "foo" and "bar", and accurately predict the UID, they can successfully execute remote code, which in practice could mean taking the credit card information from the browser and sending it to a remote server that the attacker controls.

This is further emphasized by the advisory stating that the UID has around four billion keys, making exploitation a quite feasible network attack. Let’s take a look at why this happens. To use this library, it applies the JavaScript “black sheep” function, eval, under the hood. Like in the following code:

Press + to interact
eval('('+ serialize({"foo": /1" + console.log(1)/i, "bar": '"@__R-<UID>-0__@'}) + ')');.

This code can call console.log() when the serialized version is evaluated using eval(). And because this library is mostly used to manipulate user data, either from localStorage or from an API REST request, it can cause a ton of problems if the attacker knows the code used.

The vulnerability patch

Now for some good news—this vulnerability has been patched in version 3.1.0 of serialize-javascript that means if we use any version above 3.1.0, we will not be affected by this issue and that makes our applications safe for now.

The contributors were able to address this issue by modifying the code to ensure that placeholders are not preceded by a backslash, while also incorporating a UID with a higher entropy. In terms of the severity of this issue, CVE-2020-7660 has been assigned a CVSS score of 8.1, putting it into the “important” category and close to being “critical.”

Affected projects

This is just an example, in reality, though, according to Red Hat, the risk of this vulnerability is only “moderate” since applications using serialize-javascript would need to be able to control JSON data for this bug to be triggered.

Given the wide usage of the package, other repositories have also been affected, including Ruby on Rails and Webpacker. A patch to the stable branch using a vulnerable version of serialize-javascript was issued on August 16.

How does this happen?

When bootstrapping an application, developers often rely on a variety of third-party packages and libraries to speed up the development process and avoid reinventing the wheel. These packages can provide a wide range of functionalities, from helping to manage the application’s state to handling network requests. However, as the development progresses, it’s not uncommon for developers to get caught up in the whirlwind of adding new features, fixing bugs, and optimizing performance.

In the midst of all this, updating the packages and libraries can easily fall by the wayside. It’s often not a priority, especially when there are pressing deadlines and a long list of tasks to complete. Over time, this neglect can lead to a buildup of outdated packages, each potentially harboring its own set of security vulnerabilities. This is essentially like leaving the back door of your application-wide open for attackers.

The solution

Here is a clear example of how we can make sure that our packages are always up to date:

Press + to interact
{
"name": "serialise-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.1",
"serialize-javascript": "^3.1.0"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.0.10",
"@types/react-test-renderer": "^18.0.0",
"@types/react-transition-group": "^4.4.5",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react-swc": "^3.0.0",
"babel-loader": "^8.3.0",
"css-loader": "^5.2.6",
"eslint": "^8.38.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^21.1.0",
"react-test-renderer": "^18.2.0",
"sass": "^1.58.3",
"sass-loader": "^10.1.1",
"style-loader": "^2.0.0",
"typescript": "^4.9.3",
"vite": "^4.1.0",
"vitest": "^0.28.5"
}
}

As stated above, the exploit was fixed in version 3.1, so upgrading to this version in our application package.json would ensure our app is not affected. However, imagine our application is a mix and match of hundreds of third-party packages and libraries, as is often the case with real-life applications.

How can we monitor and always be aware when any library can have a new update every day that might be a security risk? One centralized place for vulnerabilities is the National Vulnerabilities Database at NVD, where we can monitor and get alerts of software vulnerabilities in real time.

However, the optimal solution is to be pro-active and let the package.json file work, it's magic for you.

Declaring packages in package.json

The package.json file is the heart of any frontend application. It contains metadata about the application, such as its name, version, and dependencies. The dependencies are the packages and libraries that your application relies on to function.

In the package.json file, we can specify the versions of the packages we want to use. There are several ways to specify the version:

  1. Exact version: We can specify the exact version of a package that our application should use. For example, "package-name": "1.2.3" will install version 1.2.3 of the package.

  2. Tilde version: We can use the tilde (~) to specify that our application can use any version that is compatible with the specified version. For example, "package-name": "~1.2.3" will install the latest version of the package, that is, 1.2.x.

  3. Caret version: We can use the caret (^) to specify that our application can use any version that is compatible with the specified major version. For example, "package-name": "^1.2.3" will install the latest version of the package, which is 1.x.x.

While it might be tempting to always use the latest version of a package, this can introduce instability and security risks to your application. It’s often better to specify the exact version of a package that we have tested and confirmed to work with our application or to use the caret version so our application receives the most up-to-date vulnerability fixes.

The importance of regular updates

Regularly updating our packages and libraries is not just a good practice; it’s a necessity. We don’t want to reach the point where we are actually afraid of updating due to breaking changes, but we also can’t use the current versions because of vulnerabilities.

Developers often underestimate the importance of updates. They might think, “If it’s not broken, why fix it?”. However, updates are not just about adding new features or improving performance, they are also about fixing security vulnerabilities that have been discovered since the last update. Remember, security vulnerabilities are not static; they evolve over time.

Hackers are always looking for new ways to exploit applications, and the authors of the packages and libraries are continuously working to patch these vulnerabilities. By not updating our packages, we are essentially using a version of the code that is known to be exploitable.

Key takeaways

Bootstrapping an application with third-party packages and libraries can significantly speed up the development process. However, it’s crucial to remember the importance of regularly updating these dependencies to ensure the security of our application.

  • Make it a habit to check for updates regularly and integrate this practice into our development workflow.

  • Consider using automated tools that can help us manage our dependencies and alert us when updates are available.

Remember, a secure application is a successful application. Don’t let outdated packages and libraries be our application’s downfall.

The purpose of this lesson is to make us comfortable with the types of security vulnerabilities that can appear in Frontend Applications. We should always make sure to use the correct syntax in our package.json file to stay up-to-date to patch versions of packages.

The first step to mastery is being aware of what might go wrong. If we know all possibilities before we start to code, we can mitigate them and put measures in place to check if we ever have a vulnerable package.

For this in particular, there are several automated tools available that can help us manage our dependencies and stay informed about security vulnerabilities. For example, npm audit is a command-line tool that can help us identify and fix known vulnerabilities in our dependencies.