JavaScript security best practices for securing your applications
JavaScript is one of the most popular programming languages, largely because it’s an easy language for beginners. It’s easy to set up, it has an active and vast community, and users can create web, mobile, and desktop applications using only JavaScript.
But as with any programming language, bad actors try to find vulnerabilities to exploit within JavaScript applications. Common vulnerabilities include cross-site scripting, sensitive data disclosure, broken access control, session hijacking, CSRF and man-in-the-middle attacks. This blog post presents JavaScript security best practices for securing your applications.
1. Write quality code
Programming languages are all different, and knowing the ins and outs is critical to writing quality code. The U.S. Department of Homeland Security reported that “up to 90%of computer security incidents are traceable to vulnerabilities in software that were exploited by an attacker,” so it’s clear that developers need to improve the quality of their code to reduce the bugs that lead to security breaches. Some tips that might help you reduce bugs include
- Learn important concepts of the JavaScript language. Familiarity with global context, scope declarations, loose equality operators, strict equality operators, hoisting, callbacks, etc. are vital to quality code.
- Avoid functions that evaluate strings as code. JavaScript functions such as eval , Function , setTimeout , and setInterval are not recommended since they could lead to cross-site scripting (XSS) attacks if used together with untrusted data.
- Use a linter to find issues early. A linter is a tool that analyses source code for typos, logic errors, and code smells. In short, it helps you to improve the code. I recommend the ESLint linter since it is extendable and easy to start using.
- Use static application security testing (SAST) tools to detect quality and security issues. I recommend a SAST tool like Synopsys Coverity®, which provides deeper support for security issues and compliance standards. You can also use Rapid Scan Static, a feature of Synopsys Code Sight™ SE, which allows teams to perform real-time IDE-based AppSec testing without breaking developer workflows. The Code Sight plugin, available for VS Code and IntelliJ, enables developers to confirm fixes as they code to avoid work downstream.
- Convert silent errors into evident errors. Strict mode was introduced in ECMAScript 5 as an optional feature. Use this feature to convert silent errors into evident errors. For a detailed list of changes that happen when using strict mode, see this page.
- Write tests to detect defects in your thinking. Write tests to corroborate that your thinking, your code, and the expectations are all aligned. This can also serve as a documentation tool.
2. Evaluate the need of third-party libraries
Code reuse is seen as a good practice, and JavaScript developers have pushed this idea to the extreme, creating packages even for the simplest tasks. But reusing packages in a noncontrolled way exposes JavaScript applications to issues and security threats. Consider treating dependencies as code needed for the project to run. The more that is needed, the more points of failures there can be.
The following story illustrates the point. On March 22, 2016, a commonly used package implementing a basic left-pad string function (with only 12 lines of code) was deleted. The dependency chain reaction of deleting this seemingly innocent and simple package broke a big chunk of the web development ecosystem including React, Babel, and other high-profile packages.
The moral to this story is that a project is as weak as its weakest dependency. By using well-known secure libraries and frameworks, you are protecting yourself. When using less-known third-party packages, inspect who has developed it; whether the package is maintained, inactive, and well-tested; and if it has unpatched vulnerabilities. And make sure you’re installing the right package—typosquatting attacks, where malicious packages with similar names to well-known packages make their way into real applications, are common.
You should also use software composition analysis (SCA) tools to help you to detect open source license violations, vulnerabilities, and out-of-date dependencies in open source software. Black Duck® is a software composition analysis tool that helps you with detecting these issues.
3. Do not trust user input
Most web applications allow users to insert data through text input. After the data is inserted, it is reflected somewhere in the web application. But accepting and displaying inputs from users opens the door to cross-site scripting attacks in which cybercriminals use special characters to trick browsers into interpreting text as HTML markup or JavaScript code.
Output encoding transforms potentially dangerous characters into a safe form. The encoding mechanism to use depends on where the untrusted input data is placed. Some of the encoding that the Open Web Application Security Project (OWASP) recognizes are HTML entity encoding, HTML attribute encoding, URL encoding, JavaScript string encoding, and CSS Hex. Since the type of encoding to use depends on where the input data is placed, the best option is to leave these contextual encodings to the framework you are using. If you’re not using a framework, OWASP recommends using a security-focused encoding library to make sure that encodings are implemented properly.
When a web application needs to accept HTML inserted by the user, sanitize the input before displaying it on a page or sending it to other systems. HTML sanitization involves validating the input and cleaning up unexpected characters. The outcome should be a safe HTML version of the HTML input. An excellent tool to sanitize HTML is DOMPurify.
To reduce cross-site scripting attacks, sanitize inputs in addition to applying output encoding.
4. Protect yourself against JSON injection
JSON is a commonly used syntax for exchanging information between applications. It is simple; compact; easy to learn, read, and understand; and has a hierarchical structure.
An injection attack is when an attacker supplies untrusted input, without validation or sanitization, to a program or application. A JSON injection attack can impact the server side or client side. For example, when code on the server side builds a JSON object from a string, and the string is built by concatenating some inputs provided by the user. Simple string concatenation, without validation or sanitization, opens the door for exfiltration of sensitive information or misbehavior.
On the client side, this type of attack can lead to cross-site scripting if the concatenated string runs inside functions that evaluate code such as eval , Function , or setTimeout , among others. For example
In this example, an attacker could alert user cookies by injecting
To defend against this attack vector, validate and sanitize untrusted input, avoid using functions that evaluate strings as code, use the JSON.parse() function instead of eval() to parse JSON strings, and set content security policy to restrict the use of functions that evaluate strings as code.
5. Protect your cookies
HTTP cookies are used to preserve information such as authentication or session tokens, user preferences, or anything that the server should remember between requests. A HTTP cookie is a small string of data that the web server sends to the browser using the Set-Cookie HTTP header in the response. After this, the browser will automatically send the cookie back on almost every request (to the same domain) using the Cookie HTTP header.
If you do not set any flags, the cookie content is accessible programmatically using document.cookie . This is not always desirable. If an attacker is able to inject JavaScript within a web application, the script could read the content of document.cookie , and that could enable the bad actor to access any sensitive information in the cookie.
There are a variety of ways to protect cookies. If the cookie is used only by the webserver, you can use the httpOnly flag to restrict programmatical access to the cookie’s content.
HTTP does not encrypt messages, so man-in-the-middle attacks could succeed, and messages could be intercepted. If the cookie holds sensitive information, restrict the browser from sending it over unencrypted HTTP connections. Use the secure flag to instruct the browser to send cookies only through HTTPS, a protocol extension of HTTP. If you do not use this flag, the browser will send the cookie using both secure (https://my-secure-site.com) and insecure (http://my-secure-site.com) connections to a site.
In the case of a cross-site request forgery attacks, attackers can succeed because web applications can’t differentiate between valid requests and forged requests. For example, say you have a form for updating a user’s password, and the website uses cookies for handling sessions. An attacker could create a page that automatically sends a request to update your password. If cookies aren’t protected and you have a valid session cookie, when you visit this site, a request to update your password is sent from the site. Since your session cookie is valid, the browser will automatically include the cookie in the request. With this information, the webserver receives a request with a valid session cookie and updates the password. Thus, an attacker can reset your password to the value of their choice. To protect your cookies against cross-site request forgery attacks, use the cookie flag samesite=strict to ensure that the cookie is not sent when the request comes from another domain than the site that sets the cookie.
6. Defend against prototype pollution
JavaScript is a prototype-based language. When an object is created, it inherits all properties and methods following the so-called prototype chain. As it is a chain, prototypes have references to other prototypes. The chain is followed until reaching null . The Object prototype sits just below null in this prototype chain. Almost all objects in JavaScript are instances of Object .
When someone targets Object and alters the methods that most objects in JavaScript inherit through the prototype chain, this is called prototype pollution attack. It can happen in the server side or client side, and its consequences can include remote code execution, cross-site scripting, and denial-of-service attacks.
On the client side, an attacker injects code that can modify the Object.prototype directly. On the server side, the application is if it recursively clones object properties or if it sets object properties through a given path. If we have an HTTP server accepting HTTP requests, an attacker could send an HTTP request payload that contains a JSON object that tries to exploit the _proto_ , constructor , or prototype properties.
There are different ways to mitigate prototype pollution attacks.
- Freeze Object.prototype to prevent the default prototype from getting polluted.