JavaScript security: Vulnerabilities and best practices
Posted Dec 15, 2021 | 9 min. (1737 words)If you run an interactive website or application, JavaScript security is a top priority. There’s a huge array of things that can go wrong, from programmatic errors and insecure user inputs to malicious attacks.
While JavaScript error monitoring can help you catch many of these issues, understanding common JavaScript security risks and following best practices is just as important.
In this JavaScript security checklist, we’ll briefly look into the most frequent JavaScript exploits, then go through a couple of essential easy-to-implement JavaScript security best practices.
- Common JavaScript security vulnerabilities
- Security best practices
- Use a JavaScript linter
- Audit dependencies using a package manager
- Add Subresource Integrity (SRI) checking to external scripts
- Avoid using inline JavaScript
- Validate user input
- Escape or encode user input
- Use a CSRF token that’s not stored in cookies
- Ensure secure cookie transmission
- Minify, bundle, and obfuscate your JavaScript code
What are some common JavaScript security vulnerabilities?
Let’s start with three JavaScript security vulnerabilities that frequently occur in front-end development. (Note that this is, of course, a non-exhaustive list, and there are also security exploits that you’ll encounter in back-end JavaScript development (Node.js), such as SQL injection.)
Cross-site scripting (XSS)
In an XSS attack, the attacker injects a malicious client-side script into a web page. They usually achieve this by bypassing the same-origin policy of a website. As a result, the attacker can get access to user data and carry out actions on the user’s behalf.
Cross-site request forgery (CSRF)
CSRF attacks target authenticated (logged-in) users who are already trusted by the application. The attacker accesses a legitimate user’s account using the information found in session cookies and performs actions on their behalf without their knowledge or involvement. CSRF attacks are also known as session riding or one-click attacks.
Third-party security vulnerabilities
In front-end development, we use many third-party tools and libraries that are open to all kinds of JavaScript exploits. Some of these tools, such as React by Facebook, are developed and maintained by large corporations, and reliably fix issues and follow JavaScript security best practices. However, many of them are by indie developers or smaller teams that don’t always have the resources to regularly audit or update their code.
JavaScript security best practices
To help you protect yourself and your users, we’ve put together a JavaScript security checklist that includes a couple of best practices and recommends some tools that can help you eliminate common vulnerabilities and prevent malicious attacks against your website or application.
1. Use a JavaScript linter
The easiest and simplest way of avoiding JavaScript security issues is linting your code. Linters are static code analysis tools that check your code for programmatic and stylistic errors, code smells, and known security exploits.
The three most well-known JavaScript linters are JSHint, JSLint, and ESLint. Modern source code editors, such as Visual Studio Code and Atom, also come with pluggable JavaScript linting functionality.
2. Audit dependencies using a package manager
To keep third-party JavaScript security vulnerabilities in check, you need to track all the packages you’re using on your website. You can do this by using a package manager such as npm, Yarn, or pnpm.
In addition to letting you track, manage, and update your dependencies, these package managers also provide you with tools to audit your packages and find common JavaScript security issues, such as the npm audit
(see below), yarn audit
, or pnpm audit
commands that let you run code audits at different audit levels:
**
npm audit [--json] [--production] [--audit-level=(low|moderate|high|critical)]
npm audit fix [--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)]
common options: [--production] [--only=(dev|prod)]
**
3. Add Subresource Integrity (SRI) checking to external scripts
As third-party or external scripts can be easily manipulated, checking their integrity before fetching them from the external server is one of the most essential JavaScript security best practices.
Subresource Integrity (SRI) checking is a feature built into modern web browsers (see browser support) that uses a cryptographic hash to verify the integrity of an external script.
To generate the hash value, you can use a generator such as SRI Hash Generator or a command-line tool such as OpenSSL or Shasum (see the respective shell commands).
In your HTML code, you need to add the hash value you’ve generated for the external JavaScript file to the integrity
attribute of the <script>
or <link>
element. To make the SRI checking work, you also need to add the crossorigin=anonymous
attribute that makes it possible to send a cross-origin request without any credentials.
For example, I used the aforementioned SRI Hash Generator to generate the following secure <script>
tag for the React library hosted on the Cloudflare CDN.
**
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0-rc.0-next-3b3daf557-20211210/umd/react.production.min.js" integrity="sha256-9pH9+q1ELPzjhXRtae7pItnIlYsGKnDN3ragtQXNpQ0=" crossorigin="anonymous"></script>
**
4. Avoid using inline JavaScript and establish a Content Security Policy
Using inline script tags makes your website or application more vulnerable to cross-site scripting (XSS) attacks. You can avoid this JavaScript security risk by adding all your scripts, including inline event handlers (e.g. onclick
), as external .js files.
For better security, we’d also recommend that you establish a content security policy (CSP). This is a security layer in the communication between client and server that allows you to add content security rules to your HTTP response header.
If you don’t have any inline scripts on your page, it’s easier to set up a more effective CSP. You can use the script-src
and default-src
directives to block all inline scripts, so if any malicious inline script tries to execute on your site, it will automatically fail.
5. Validate user input
Validating user input on both the client- and server-side is essential to avoid malicious code injections.
HTML5 forms come with built-in form validation attributes such as required
, min
, max
, type
, and others that let you check user data and return error messages without any JavaScript on the client side. You can also use the pattern
HTML attribute to validate the value of an input using a Regular Expression.
In addition to these HTML5 attributes, modern browsers also come with support for the Constraint Validation API that lets you perform custom input validation using JavaScript.
This is a web API that extends the JavaScript interfaces belonging to different HTML elements used in forms, such as HTMLInputElement
, HTMLSelectElement
, and HTMLButtonElement
and provides useful properties and methods for checking input validity against different constraints, reporting validity status, and performing other actions.
6. Escape or encode user input
To avoid XSS attacks, it’s also important to escape or encode incoming or unsafe data. Escaping and encoding are two technologies that convert special characters that can pose a security risk into a safe form.
While encoding adds an extra character before a potentially dangerous character, such as the \ character before the quotation mark in JavaScript, escaping converts a character into an equivalent but safe format, for instance the > character into the >
string in HTML.
As a rule of thumb, you should always encode HTML entities, such as the < and > characters, when they come from untrusted sources. To escape URIs and JavaScript code, you can use free escaping/encoding tools such as the JavaScript String Escaper and URL Encoder/Decoder by FreeFormatter.
It’s also best to avoid using JavaScript properties and methods that return unescaped strings. For example, you can use the safe textContent
property instead of innerHTML
which is parsed as HTML (therefore the characters are not escaped).
7. Use a CSRF token that’s not stored in cookies
If user authentication in your application is based on cookies, a malicious attacker can get access to your session cookies and act on behalf of an authenticated user. As mentioned above, these CSRF attacks are among the most common JavaScript security vulnerabilities.
You can prevent this JavaScript security issue by sending an additional token with each HTTP request. As these CSRF tokens are not stored in cookies, the attacker can’t access them. You can add CSRF tokens to forms, AJAX calls, HTTP headers, hidden fields, and other places.
For instance, here’s an example of a CSRF token by the OWASP project that you can add to a form as a hidden input field:
**
<form action="/transfer.do" method="post">
<input type="hidden" name="CSRFToken" value="OWY4NmQwODE4ODRjN2Q2NTlhMmZlYWEwYzU1YWQwMTVhM2JmNGYxYjJiMGI4MjJjZDE1ZDZMGYwMGEwOA==">
[...]
</form>
**
8. Ensure secure cookie transmission
To further improve cookie security, make sure that your cookies are only transmitted via a secure protocol such as HTTPS that encrypts the data sent between the client and server machines. You can enforce the use of a secure protocol by adding the ;secure
flag to the Document.cookie
property that gives you access to the cookies of a document.
You can use it together with the ;samesite
flag that lets you control cookie transmission in cross-site requests. For example, using this flag with the lax
value allows cookie transmission for every same-site request and all top-level navigation GET requests, which makes user tracking possible but prevents a significant portion of CSRF attacks (this is the default browser setting for the ;samesite
flag).
You can use the ;secure
flag in the following way (here, ;samesite
is set to none
, which allows cookie transmission for all cross-site and same-site requests):
**
// Source: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#example_2_get_a_sample_cookie_named_test2
document.cookie = "test=Hello; SameSite=None; Secure";
**
9. Minify, bundle, and obfuscate your JavaScript code
Finally, you can make it harder for hackers to understand the structure and logic of your scripts by minifying and bundling your code using a tool like Webpack that comes with further security features. For example, you can add a nonce to every script it loads.
While minifying and bundling scripts is generally seen as a JavaScript best practice, obfuscation is a controversial topic. This is because it takes longer for the browser to load obfuscated scripts, which detracts from performance and user experience, especially at a higher obfuscation level. However, if you still decide to obfuscate some or all of your scripts, you can use a free tool such as Obfuscator.io that also has plugins for popular tools such as Webpack, Grunt, Rollup, Netlify, and others.
Conclusion
Following these JavaScript security best practices can help you make your scripts safer and prevent common attacks, such as cross-site scripting, cross-site request forgery, third-party security vulnerabilities, and others.
We also recommend that you use real-time JavaScript error tracking to see the issues your users encounter on your website or application in production. Data returned by real user monitoring tools can reveal additional JavaScript security risks, code smells, and other vulnerabilities you didn’t notice in the development phase, and allow you to fix them before an attacker finds and exploits them.