JavaScript is the front-end of the entire internet. Whether you transpile TypeScript down into JavaScript, create fast little node.js scripts, or build a beautiful-but-dumb front end that calls a much more interesting collection of APIs, it’s literally everywhere. Because JavaScript is so prolific, it’s a prime target for attackers. In this article we will cover ten tips for writing more secure JavaScript.
1. Cross-site scripting
The number one item to discuss when it comes to JavaScript security is always cross-site scripting (XSS). Cross-site scripting is a form of injection; it means an attacker has confused your application into either interpreting or executing their malicious code instead of treating it as data. User input should always be treated as data, but unfortunately computers can be fooled if we are not careful.
XSS is the one type of injection that works only in JavaScript. It is also the only type of injection that attacks the user directly, by taking control of the browser and using it against the victim. All other types of injections do not attack the user; for instance, SQL injection attacks the database server, command injection attacks the host operating system that the system is running on, and LDAP injection attacks the LDAP server. You get the picture.
With XSS, an attacker can use the browser to access your cookies (including your session information, if you have stored it inside your cookie in an insecure way), external scripts (if you haven’t locked that down using Content Security Policy), install a keylogger, vandalize your website, etc. Anything that JavaScript is cable of doing, an XSS attack can also do; the only limits are an attacker’s imagination.
Although instances of XSS have declined over the years, thanks to various forms of awareness and education (such as The OWASP Top Ten Risks to Web Apps), newer JavaScript frameworks performing output encoding automatically, and teams taking security more seriously than ever before, it is unfortunately still a high risk problem.
To eliminate your chances of having XSS affect your applications, perform the following actions:
- Perform input validation on all user-supplied or user-modifiable data. After you perform input validation, if you have to accept potentially hazardous characters (such as <, >, ‘, “, -, etc.) you should protect your application by either escaping them (adding a backslash in front) or sanitizing them out (literally replacing them with another character or just removing them altogether).
- Perform output encoding on anything that will be displayed to the screen, including anything that might be displayed (for instance if you are returning something from an API that you know will be displayed by your front end). If you can have your framework do this work for you, that’s the easiest and often most effective way to ensure you do this correctly. Output encoding can become quite complicated, especially if you are doing inline JavaScript. No one wants to do nested encoding!
- Use the Content Security Policy header (CSP) to list all of the third-party components that you will allow as part of your app, especially scripts. The first thing a malicious XSS attack will do is try to call out to another, much larger, malicious script on the internet. Most fields only allow 50 or 100 characters, which is not a lot of space to write code for an attack. If they are able to call out to a malicious site on the net, then call a much longer script, your increase the risk exponentially.
- Add the
httpsOnly
flag to your cookies to ensure that if you did manage to miss something, the attacker will never be able to access your session information from your cookies. Trying to access your cookies is often the second thing a XSS attack will try to do, so don’t let them access that sensitive information. - Perform manual code review, use a static (SAST) or dynamic (DAST) analysis tool, or perform a penetration test to be absolutely sure that you didn’t miss anything!
2. Use JavaScript frameworks that output encode for you
React, Angular, and Vue.js automatically perform output encoding, and also have many, many other amazing features. When using any framework, be careful to check if there are dangerous functions that you should avoid or be careful with, such as React’s dangerouslySetInnerHTML
and Angular’s bypassSecurityTrustAs*
functions.
3. Avoid inline scripting
As tempting as it may be to include JavaScript directly within your HTML if you’re “just trying to do something really quick”, it greatly increases the possibility of XSS (and creates maintenance issues for later). On top of that, it’s a mess. Keep your JavaScript in separate external files to maintain that extra layer of security and organization. You can define your scripts in a CSP header to keep everything nice and orderly. This is similar to inline SQL, when we combine user input and then feed it directly to the database to execute…dangerous situations happen.
4. Use strict mode
We are not neanderthals, which means of course we want to write nice, clean, syntactically correct code. Strict mode helps us do this by tightening the language’s rules to help you write safer, cleaner, and more predictable code. You should be using strict mode in all languages that it is offered, not just JavaScript.
Strict mode prevents silent errors, disallows unsafe actions, blocks you from using reserved words (such as let
) for anything other than what they were intended, prevents accidental global variables, and it makes the use of this
safer and more predictable. Plus, strict mode is also supposed to improve performance, help you catch bugs earlier, steers you toward best practices, and it improves security.
5. Use open source tools
Many of these risks are well-known, and developers in the open-source community have created tools that mitigate them. Use helpful, free and open-source software (FOSS) libraries and tools to help you write more secure code, such as:
- DomPurify for sanitizing input against XSS.
- Retire.js to look for outdated, unsupported, or otherwise dangerous JavaScript libraries.
- Npm Audit, Yarn Audit, Snyk CLI (free version) to check your dependencies are up to date.
- DOM Snitch is a Google project which created an experimental Chrome extension that enables developers and testers to identify insecure practices commonly found in client-side code.
- Nodejsscan a free static analysis tool for node.js apps.
- Semgrep Community edition, Bearer CLI (free version), Horusec, etc. to perform free static analysis. They all work on several languages, including JavaScript.
- Zap to perform free dynamic analysis (DAST) Note: get permission from your boss before running Zap at work! This is a hacker tool and running it against any website or app without written permission is illegal. This tool can cause damage if used incorrectly. Please read up on it before you use it. The other tools are never dangerous to use.
6. Clearly identify text within code
Be clear in your code that text is text, and that whatever is inside your variable is not code that should be executed or interpreted. We ensure that text is text by putting anything from the user into a safe data element for your JavaScript or CSS. For example, do not use innerHtml
(which is rendered); instead use innerText
or textContent
, which are clearly text-only, for display, and not to be interpreted. Always be clear that something is text; don’t let the DOM (document object model) decide, because sometimes it gets it wrong.
7. Apply variables to safe attributes only
If you must set element.setAttribute
with data from a variable (which is often a user supplied value, and is therefore potentially dangerous), only use safe, static attributes, rather than attributes that are dynamic (changeable). Examples of safe attributes include: align
, alink
, alt
, bgcolor
, border
, cellpadding
, cellspacing
, class
, color
, cols
, colspan
, coords
, dir
, face
, height
, hspace
, ismap
, lang
, marginheight
, marginwidth
, multiple
, nohref
, noresize
, noshade
, nowrap
, ref
, rel
, rev
, rows
, rowspan
, scrolling
, shape
, span
, summary
, tabindex
, title
, usemap
, valign
, value
, vlink
, vspace
, width
). Do not use dynamic, unsafe attributes for element.setAttribute
, such as onclick
or onblur
. Attributes where something ‘happens’ or changes are not safe, and you should not use them with user-supplied data (such as whatever data is saved into a variable).
While we’re on the topic of using variables (user data)…they should only be placed inside CSS property values, not into contexts. Again, we don’t want them potentially making our software act in unpredictable ways.
Example: <style> selector { property : $varUnsafe; } </style>
8. Validate input on the backend
When performing input validation for security reasons (not for usability, speed, or any other reason), the checks must be performed on the backend, not in the front end. Anyone with an intercept proxy, such as Zap or Burp Suite, is able to ‘intercept’ your front end’s request, make changes to it, and then pass it onto the backend as though it was not interfered with. If done well, the backend has no idea if someone made any changes to a request after it left the front end. This type of attack or testing would be performed on the same machine as the person using the application, meaning they would not need to worry about encryption, they would have direct access in clear text to your entire request.
Why is this bad? Imagine you have input validation in your JavaScript that says you are willing to accept a-z, upper and lower case, as well as all numbers, but nothing else. It accepts this data and then reflects to back to the screen for the user to read. The app would theoretically reject the following input “‘ or 1=1 --”
because several of those characters are not on your allowlist
. However, imagine an attacker is using your application with an intercept proxy. The attacker enters “Hello” into the field, and it passes the input validation. They then intercept your request, change the field to say “ ‘ or 1=1 --”
, and then sends it onto your application. If there was no input validation on the backend, the application would use that data however it normally would, in this case potentially appending it to an SQL statement such as ‘select * from table users where username like %’ & your_now_malicious_variable_here & ‘ and password = ‘ & password_variable
.
Although this is a simplified example, hopefully you can see that input validation, for security reasons, must be performed on the backend. You can always perform it on the front-end as well, for speed and usability. You just need to ensure the final say is always on the server.
9. Avoid problematic functions
Avoid the following, often-problematic, JavaScript functions. Especially and specifically if you are going to use them with user supplied or user-modifiable data. If you must use them, proceed with extreme caution. eval()
, innerHTML
, outerHTML
, Function()
, escape()
, unescaped()
, document.write()
, document.writeln()
, unescapeHTML()
, decodeURI()
and encodeURI()
, with()
, constructors that use a string argument (which means a variable), setTimeout()
, setInterval()
, new function
, and setAttribute()
(recall: if you use setAttribute
, use static values only).
10. Secure it like any other application
Perform all the other secure coding strategies that you would for any other application, such as performing input validation / sanitization / escaping, using parameterized queries instead of inline/dynamic query building, encrypting your data in transit and at rest, and using reliable systems for authentication, authorization, session management, identity, and secret management, etc.
Of the standard items that most people consider to part of ‘secure coding’, ensuring that your team follows a secure system development life cycle (S-SDLC) is the absolute most important one. When I say a secure SDLC, I mean that your team adds additional activities and processes to ensure that the software you are building and maintaining is safe, reliable, and rugged. Some of the activities you could add to any methodology would include threat modeling, secure code review, automated dynamic testing, software composition analysis or other verification of your third party dependencies, hardening of your software supply chain, creating security user stories, or including security requirements alongside all your other project requirements. Any security activities that you add to your SDLC will help ensure we create more robust and trustworthy systems that your users can count on. The more you add the better!
The journey to writing more secure JavaScript begins with small, consistent changes. Choose one or two best practices and put them into action today. As you start to see their impact, take a moment to share what you’ve learned with a friend or colleague. Through knowledge sharing and incremental improvements, we can make JavaScript not only more beautiful and powerful, but also safer for everyone.