Defensive JavaScript

Many thanks to Google’s Mike Samuel for contributing this article on JavaScript security.

How to write code that doesn't do what it oughtn't

Sometimes we developers are asked to wear many different hats. I've felt like I needed to be a graphic designer to craft CSS, an anthropologist when dealing with forty languages worth of I18N/L10N, or a detective when piecing together logs and git history to find a heisenbug in legacy code.

Rather than asking you to wear yet another hat, here are a few approaches I take when wearing my security engineer hat that might help you write code that you and your friendly, neighbourhood blue teamers can have confidence in.

Make the language work for you

Imagine you were asked to review this code:

    <a name="makelink-1"></a>   
    /** Returns a link given two strings: a fragment of HTML, and a URL. */
    function makeLink(linkTextHTML, url) {
      return `<a href="${ url }">${ linkTextHTML }</a>`;
    }

This does what it claims to, but it is still deeply flawed.

    // What if an attacker controls the inputs?
    const x = '<script>alert(1)</script>';
    const y = 'javascript:alert(1)';
    
    console.log(makeLink(x, y));
    // -> <a href="javascript:alert(1)"><script>alert(1)</script></a>

It is tightly coupled to the security assumptions of code that calls it. Yes, the inputs are strings, but they cease to be just strings when a browser interprets parts as privileged JavaScript.

When you have an important distinction, define distinct types. Many programs need to distinguish between strings that are essentially extensions of the program from those that are not.

    /** Returns a link as TrustedHTML given some TrustedHTML link text and a TrustedURL. */
    function makeLink(linkTextHTML, url) {
      // If linkTextHTML is TrustedHTML it doesn't change.  Otherwise escapes it.
      linkTextHTML = TrustedHTML.escape(linkTextHTML);
      // If url is a TrustedURL it doesn't change.
      // Otherwise allow it if it uses a common, safe scheme.
      url = TrustedURL.sanitize(TrustedURL);
      // We need to keep an eye on code that creates TrustedHTML.
      return toTrustedHTML(`<a href="${ url }">${ linkTextHTML }</a>`);
    }

This is more verbose, but this function isn't tightly coupled to its clients' unchecked security assumptions. (Later I show how to keep code like this small)

Trust process not filters

Imagine you were responsible for helping developers produce secure code. Would you rather hear:

    > We trust values that match carefully crafted boolean expressions. 
    <br>
    > For example, our `> > > makeLink`> > > function is safe because its output matches `> > > /^<a href="(?=https?:|mailto:|\w*(?![:\w]))[^"]*">[^<]*<\/a>$/`> > > . 
    <br>
    > As our code evolves, we'll change our boolean expressions.

or

We trust outputs of reliable tools wielded by trusted developers. As our code evolves, we'll recombine these tools in new ways, and carefully vet any new tools.

The first argument is based on filtering. It's really hard to get right (hint: in HTML ':' and '\:' mean the same thing), and as a program evolves filters tend to weaken over time since false-positives are easier to find during testing than false-negatives.

The second argument is for security by construction which is much more flexible. It's easier to balance features and safety when inputs are trusted because of how they came to be, not how their bits are arranged.

Check assumptions before doing things you can't undo

To find the root cause of vulnerabilities, security engineers look for chokepoints: a small number of functions that are called when a program does something it shouldn't and which can't be undone.

    // For example, on the server
    { response.write(x); }    // can't be undone.
    // and on the client
    myDOMNode.innerHTML = x;  // can't be undone.

It would be bad if an attacker could cause

    x = '<img onerror="alert(1)" src="bogus">';

A program that keeps untrustworthy values away from these chokepoints is more robust. The type system can help us avoid confusing trustworthy and untrustworthy strings.

    if (!TrustedHTML.is(x)) {
      throw new TypeError('Expected TrustedHTML but got ' + util.inspect(x));
    }

Fyi, the Trusted Types proposal identifies chokepoints in the DOM and it monkeypatches to add checks around chokepoints. In Google, we use JSConformance to guide developers away from these chokepoints and towards safe wrappers.

Use tools that you can rely on

The makeLink(...) example is a bit contrived. (The author wanted one example to build on throughout the article.)

    <a href="{{ url }}">{{ linkText }}</a>

That's a lot nicer. Tools like template languages make our lives easier, but if the template language is naïvely adding strings, then you're no better off than the first makeLink example.

If an HTML template language can't produce a simple link without risk of XSS, then it's a poor tool.

Template languages that understand the different contexts in which url and linkText appear can respect your decisions when given TrustedHTML or a TrustedURL while ensuring safety when dealing with plain old strings.

HTML isn't the only language that Node programs produce. In "A Roadmap for Node.js Security" I argued that tagged string templates balance security and ease of use.

For example, safesql understands the syntax of SQL strings, and an sh tag understands bash and sh syntax so you can use its output with child_process.

    const { mysql } = require('safesql');
    // ID is auto-escaped using SQL conventions.
    const whereClause = mysql`WHERE id=${ id }`;
    // The output is a trusted string of SQL so it's not over-escaped.
    const query = mysql`SELECT * FROM Table ${ whereClause }`;
    
    const { sh } = require('sh-template-tag');
    const shellCommand = sh`tar xfz ${ tarball }`;

Plan for failure

None of us is always our best self. I make more mistakes when tired, and sometimes forget to come back to code that I planned to rework. If you're only secure when no team member makes a mistake, look for a different approach.

Module privilege grants lets a team collectively keep the amount of code that can create Trusted values small, and codeowners can help a team ensure that changes to that small amount of code gets extra review. These practices can help get multiple pairs of eyes where they're needed consistently.

But having multiple pairs of eyes doesn't help if the review burden becomes exhausting. Let's re-examine our earlier code example:

    /** Given some TrustedHTML and a TrustedURL returns a link as TrustedHTML. */
    function makeLink(linkTextHTML, url) {
      // If linkTextHTML is TrustedHTML it doesn't change.  Otherwise escapes it.
      linkTextHTML = TrustedHTML.escape(linkTextHTML);
      ...
    }

The checks we added solved a security problem, but the developer had to put in the right checks. If code that adds security checks shows up in code reviews daily, then even the best team probably makes security-relevant mistakes monthly.

Ideally, code that enforces security properties is reviewed carefully which means it needs to change rarely. If you notice too much churn, try to move security checks into lower-level, lower-churn code. Ideally into a framework or framework plugin.

Buggy code can't leak what it doesn't have

Imagine a colleague writes a webservice that responds with JSON when a user asks for their friend list.

They test the web service and see that it produces an array like [ { "displayName": "ILikeCats", "userId": "A01F" } ].

Later I want to produce more a more detailed response for the current user. I add a homeAddress field to the same class Account that my colleague used.

We tend to assume that adding to an API won't break existing code but my additive change now causes my colleague's request to leak PII: [ { "displayName": "ILikeCats", "userId": "A01F", "homeAddress": "..." } ]

"Bags of Properties" talks about this problem and how to avoid unintentional leaks via symbols and boxes.

Treat JavaScript like a dynamic language

TypeScript's static types help catch bugs early, but they're not sufficient for security.

You might see the checks in the second version of makeLink and be tempted to use static types instead of runtime checks:

    function makeLink(linkText: TrustedHTML, url: TrustedURL): TrustedHTML {
      return toTrustedHTML(`<a href="${ url }">${ linkTextHTML }</a>`);
    }

That's a fine change, but you shouldn't remove the code that checks the inputs. TypeScript makes pragmatic assumptions so that, even when there are no type errors, a variable's value may not correspond to its type.

If you're not convinced ( or you're a fan of TS trivia :):

  • Shape facts defines a function that returns a number, except when an attacker abuses it to load arbitrary code.
  • Type confusion defines a "trusted" type class, but TypeScript doesn't catch type violations.
  • “A very simple piece of code” invites you to try to figure out how to attack a piece of JS that is still vulnerable when types are added.

Layer defenses

I used XSS throughout as an example of a security problem since developers are familiar with it, but some might say

Why should I worry about XSS? I use a strong Content-Security-Policy.

I wouldn't say "I'm wearing a hardhat so I don't need safety goggles." When you layer defenses, in this case Content-Security-Policy and safe coding practices, an attacker has to bypass both to affect your users.

Ask for help

Computer security is its own field with its own weighty textbooks, so don't expect to learn everything.

We security hat wearers may seem a tad paranoid, but we're really friendly people, so don't hesitate to reach out.

There are plenty of places to find help. For example:

If there's any that you found particularly helpful, mention them in the comments.

Finally, if you do want to try on a security hat, "Puzzling towards security" is a video series where I present a series of JS related capture-the-flag style puzzles, and I'm inviting attackers to test a hardened Node.js stack.

Thanks for reading :)

Want more? Learn about security in the cloud.

The contributors to JavaScript January are passionate engineers, designers and teachers. Emily Freeman is a developer advocate at Kickbox and curates the articles for JavaScript January.