CSS-in-JS security: the rascal's guide

Update February 2019: Version 3 of Rebass solves the issue mentioned in this article, and glamorous has been deprecated by its maintainer. However, styled-components (and emotion) still don’t perform any auto-escaping of interpolated variables - so be careful!*

CSS-in-JS is an exciting new technology that completely eliminates the need for CSS class names. It makes it possible to add styles directly to your components, using the full power of CSS. Unfortunately, it also promotes interpolation of unescaped props into that CSS, opening you up to injection attacks.

And CSS injection attacks are a major security hazard.

If your site or app accepts user input and displays it to others users, usage of CSS-in-JS libraries like styled-components or glamorous may result in your site being defaced. But worse, you may inadvertently allow attackers to make requests from your user’s machines, siphon their data, steal their credentials, or even execute arbitrary JavaScript.

Of course, it is also possible to use CSS-in-JS safely. You’ll just need to follow one simple rule.

CSS-in-JS tools are like eval for CSS. They’ll take any input and evaluate it as CSS.

The problem is that they’ll literally evaluate any input, even if it is untrusted. And to make matters worse, they encourage untrusted input, by allowing you to pass in variables via props.

If your styled components have props whose value is set by your users, you’ll need to manually sanitize the inputs. Otherwise malicious users will be able to inject arbitrary styles into other user’s pages.

Say you want to allow users to pick the color of their profile page, just like you can on Twitter. This would be a pain in the ass with plain CSS, but CSS-in-JS makes it easy; you just add a color prop!

As it happens, a backend developer has kindly handled the API side of things for you, and now you have a color prop available in your styled components.

As your app is a single page app, the login form opens as an overlay over the profile. And since your backend developer stored the color in a text field without validation, a malicious user can now set a color that will steal some users’ passwords:

This only works because the tools run something like the CSS equivalent of eval on the interpolated string. If you use standard inline style, or always remember to sanitize your inputs, you’re safe.

This works by using attribute selectors on the password field to change the background image depending on the current input. Here’s what Chrome dev tools’ network tab looks like after I type in 'password’:

While this attack can’t steal all passwords, it’ll still get quite a few of them. And a few stolen passwords is more than enough to ruin your day.

Say that your boss wants each user in your application to have an avatar next to their name. Fair enough. Your boss, being a bit stingy, doesn’t want to pay for bandwidth for the avatars. So he wants you to provide the option to hotlink to an off-site URL. Whatever.

Naturally, your Identity component is built as a styled component using glamorous. It accepts an entire user object as a prop, with name, twitter and a few other things. Your backend developer adds an avatarURL to the object, and then your designer adds an image to the markup using a background-image tag.

And now, anybody who views that avatar will have data from specific elements on their page hoovered up by god-knows-who. Here’s the avatarURL that does it:

This almost looks like a good old fashioned SQL injection, but with CSS. It’s amazing how history goes in cycles.

A bug has been reported to the chrome team by the linked article’s author, but it has been marked as WontFix.

This works by attaching different URLs to each character in a custom font, then applying that font to the text you want to siphon. This allows you to get a list of characters, and if applied to an input as the user is typing, it’ll be guaranteed to give you them in the correct order, along with timing information. You can also combine with things like the ::first-letter or ::selection selector to get more detailed information.

A look at Chrome dev tools’ network tab shows how the current user’s name was extracted:

If you have users in IE9, and somehow a malicious user manages to upload a file and inject the associated behavior attribute into a stylesheet via an unsanitized prop, then a malicious user can steal their accounts.

I’m not going to go into a demo, but please understand that this type of attack has happened before in the wild. You can read more about the details at Executing JavaScript Inside CSS.

One of the great things about React is that it lets you create components, facilitating separation of concerns. Child components don’t need to know where their props come from. Parent components don’t need to know how their children are implemented. Components are independent, improving maintainability and reusability.

Unsanitized props break this independence.

For example, consider a component that accepts two props: an unsanitized theme prop that is interpolated into the stylesheet, and a content prop:

From a quick glance at the component’s signature, it isn’t immediately obvious whether theme or content are sanitized and/or used in the stylesheet. Indeed, even looking at the implementation doesn’t tell us how theme is used.

To ensure that your components stay reusable and maintainable, use a naming scheme to make it clear when props are dangerous. For example:

The only way to know whether a prop in a third-party library is safe is to dive into the source code and check.

For example, consider this 3rd-party tooltip component:

<Tooltip
position="left"
content={userAddress}/>

While you may assume that it is safe to pass user details like a name or address to the content prop, you cannot actually know if it’s safe until you check the source.

You may feel like this is a rather contrived example, but it is actually a reported security issue in a popular UI toolkit based on styled-components — whose author doesn’t consider it an issue worth fixing. Let me repeat that for emphasis: if you pass user input to the content field of a popular <Tooltip>, you’ve opened yourself to all of the above mentioned attacks, because the designer does not consider it to be a security issue.

For what it’s worth, the issue was reported over a year ago at the last update, and then closed because:

The safest way to add styles based on user input is to use plain old inline style, i.e. the style prop. Anything you put in a style object is safe.

But if inline style does not suffice, you’ll need to manually escape each occurence of user input using CSS.escape. This utility is a relatievly new standard, so you’ll need to use a polyfill.

Keep in mind that all it takes is a single unescaped prop to ruin your day. Because of this, if you’re going to interpolate any props that contain user input, the only safe approach is to escape every prop — over your entire application.

One excuse I’ve heard is that all of these issues are the backend developer’s fault; they should sanitize the data before they store it. Naturally, I heard this excuse from a frontend developer.

Security is everyone’s problem. While most of us make every effort to do the right thing and sanitize input properly, people make mistakes. We’re human. And that’s why it is irresponsible to assume that the backend will always supply clean data, just as it is irresponsible to assume the same of the frontend.

Yes. This is because JSX does not trust interpolated strings by default. It only let’s you dangerously insert HTML if you use the dangerouslySetInnerHTML prop, and pass an object with the format { __html: 'your_string' }.

Nobody ever means to let unfiltered user input into HTML. But people make mistakes, and that’s why React requires you to explicitly tell it that directly interpolated strings are safe.

Currently, CSS-in-JS does not provide any mechanism for automatic sanitization (but there is talkabout it). So until it does, make sure to name any interpolated props as unsanitizedSomething.

And ideally, avoid using interpolated props altogether.

Go Pro

Stay in the loop.

Keeping up to date with React is a full time job that pays only in frustration. Luckily, you can delegate! Just become a free member, and we'll keep you up to date with our monthly newsletter.