Securing the front end, from a Node server

Gleb Bahmutov

our planet is in imminent danger

https://lizkeogh.com/2019/07/02/off-the-charts/

+3 degrees Celsius will be the end.

we have to act today

ME

you

you

you

you

vote & lobby & rebel

https://citizensclimatelobby.org

if I can help I will

@bahmutov

gleb.bahmutov@gmail.com

If there is a company that fights global climate catastrophe and needs JavaScript and testing skills - I will do for free.

Take a deep breath

Did You Know:

You can skydive without

a parachute ...

but only once

Did You Know:

You can have your website hacked ...

but only once

What happens to my company if its site runs hacker's code?

This presentation will teach you how to make your web app secure against a very common attack called script injection (XSS)

WHY / WHAT

Agenda

  • How script attacks happen on the web
  • Prevent attacks with Content-Security-Policy (CSP)
  • How to implement CSP on your Node servers

 

Gleb Bahmutov

15 years in development

MathWorks, Kensho, VP of Engineering at Cypress.io

100s of open source projects, blog posts mostly JavaScript

these slides

Inspiration

curl -I https://github.com/

GitHub headers

$ curl -I https://github.com/
HTTP/1.1 200 OK
Server: GitHub.com
Date: Thu, 21 Jan 2016 00:23:13 GMT
Content-Type: text/html; charset=utf-8
Status: 200 OK
Cache-Control: no-cache
Vary: X-PJAX
X-UA-Compatible: IE=Edge,chrome=1
Set-Cookie: logged_in=no; domain=.github.com; path=/; expires=Mon, 21 Jan 2036 00:23:13 -0000; secure; HttpOnly
X-Request-Id: 49ac67736ceec9bf127572e7c42c8235
X-Runtime: 0.007182
Content-Security-Policy: default-src *; base-uri 'self'; 
connect-src 'self' live.github.com wss://live.github.com uploads.github.com status.github.com 
api.github.com www.google-analytics.com api.braintreegateway.com client-analytics.braintreegateway.com 
github-cloud.s3.amazonaws.com; font-src assets-cdn.github.com; form-action 'self' github.com 
gist.github.com; frame-src 'self' render.githubusercontent.com gist.github.com checkout.paypal.com; 
img-src 'self' data: assets-cdn.github.com identicons.github.com www.google-analytics.com 
checkout.paypal.com collector.githubapp.com *.githubusercontent.com *.gravatar.com *.wp.com; 
media-src 'none'; object-src assets-cdn.github.com; script-src assets-cdn.github.com; 
style-src 'self' 'unsafe-inline' 'unsafe-eval' assets-cdn.github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Public-Key-Pins: max-age=300; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; pin-sha256="JbQbUG5JMJUoI6brnx0x3vZF6jilxsapbXGVfjhN8Fg="; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
Vary: Accept-Encoding
X-Served-By: d0e230454cb69aa01d4f86fc3a57b17f
X-GitHub-Request-Id: AC552DBA:3AD3:FB489A:56A024F1

Content-Security-Policy

Content-Security-Policy:
  default-src *;
  script-src assets-cdn.github.com;
  object-src assets-cdn.github.com;
  style-src 'self' 'unsafe-inline' 'unsafe-eval' assets-cdn.github.com;
  img-src 'self'
    data: assets-cdn.github.com www.google-analytics.com ...;
  media-src 'none';
  frame-src 'self'
    render.githubusercontent.com gist.github.com www.youtube.com ...;
  font-src assets-cdn.github.com;
  connect-src 'self' live.github.com ...;
  base-uri 'self';
  form-action 'self' github.com gist.github.com

Why?

Name:

John Doe

<span>{{ name }}<span>

<span>John Doe<span>

Because you want to show personal greeting to a user ...

Name:

<script>alert('hi')</script>

<span>{{ name }}<span>

<span><script>alert('hi')</script><span>

innerText or innerHTML?

Because you want to show personal greeting to a user ...

innerText or innerHTML?

Direct text or escaped?    ">" or "&gt;"

innerText or innerHTML?

Dangerous, but has styling, elements, etc

Really dangerous if one user's content

can be viewed by another user

See XSS attack yourself

My Site

My content

 

 

 

 

 

 

 

 

User comments

Ads, etc

 

 

 

 

 

 

 

<script>stealStuff()</script>

Will a script embedded in the user-entered content be executed (for another user)?

How bad things happen to good people

// server
const app = koa()
app.use(function * () {
  this.render('index.jade', {
    greeting: 'Hello world'
  })
})
// Jade template
doctype html
html(lang="en")
  head
    title Example
  body
    h1 #{greeting}
<body>
  <h1>Hello world</h1>
</body>
// server
const app = koa()
app.use(function * () {
  this.render('index.jade', {
    greeting: '<script>alert("1");</script>'
  })
})
// Jade template
doctype html
html(lang="en")
  head
    title Example
  body
    h1 #{greeting}

Jade variables are HTML escaped by default

<body>
 <h1>&lt;script&gt;alert(&quot;1&quot;);&lt;/script&gt;</h1>
</body>
// server
const app = koa()
app.use(function * () {
  this.render('index.jade', {
    greeting: '{{ a + b }}'
  })
})
// Jade template
doctype html
html(lang="en")
  head
    title Example
  body
    h1 #{greeting}
<body>
 <h1>{{ a + b }}</h1>
</body>
// server
const app = koa()
app.use(function * () {
  this.render('index.jade', {
    greeting: '{{ a + b }}'
  })
})
// Jade template
doctype html
html(lang="en")
  head
    script(src="https://code.angularjs.org/1.0.1/angular-1.0.1.js")
    title Example
  body(ng-app)
    h1 #{greeting}

Angular template

<body ng-app="" class="ng-scope">
    <h1 class="ng-binding">0</h1>
</body>

"AngularJS better not execute {{ <script>...</script> }} tags or I will loose it!"

AngularJS does NOT execute {{ <script>...</script> }} tags

// server
const app = koa()
app.use(function * () {
  this.render('index.jade', {
    greeting: 'Hi {{constructor.constructor(\'alert(1)\')()}}'
  })
})
// Jade template
doctype html
html(lang="en")
  head
    script(src="https://code.angularjs.org/1.0.1/angular-1.0.1.js")
    title Example
  body(ng-app)
    h1 #{greeting}

Template is NOT in DOM

Runs on every digest cycle

Will never happen to me

Angular 1.x edition

{{constructor.constructor('alert(1)')()}}

fixed in Angular 1.2

Will never happen to me

{{ (_=''.sub).call.call({}[$='constructor'].getOwnPropertyDescriptor ( _.__proto__,$).value,0,'alert(1)')() }}

Will never happen to me

{{ objectPrototype = ({})[['__proto__']]; objectPrototype[['__defineSetter__']]('$parent', $root.$$postDigest); $root.$$listenerCount[['constructor']] = 0; $root.$$listeners = [].map; $root.$$listeners.indexOf = [].map.bind; functionPrototype = [].map[['__proto__']]; functionToString = functionPrototype.toString; functionPrototype.push = ({}).valueOf; functionPrototype.indexOf = [].map.bind; foo = $root.$on('constructor', null); functionPrototype.toString = $root.$new; foo(); }} {{ functionPrototype.toString = functionToString; functionPrototype.indexOf = null; functionPrototype.push = null; $root.$$listeners = {}; baz ? 0 : $root.$$postDigestQueue[0]('alert(location)')(); baz = true;'' }} 

Will never happen to me

{{ 'this is how you write a number properly. also, numbers are basically arrays.'; 0[['__proto__']].toString = [][['__proto__']].pop; 0[['__proto__']][0] = 'alert("TROLOLOLn"+document.location)'; 0[['__proto__']].length = 1; 'did you know that angularjs eval parses, then re-stringifies numbers? :)'; $root.$eval("x=0", $root); }}

Still in ng 1.4.5

Lesson: you cannot sanitize your way out of <script> tags

We need to stop confusing code from data

<script> // this is code </script>
<div>This is data</div>
<script src="path/to/code.js"></script>
<button onclick="code()">Log in</button>
<div style="background:url('javascript:alert('xss')')">

code

code

code

code

<script> // this is code </script>
<div>This is data</div>
<script src="path/to/code.js"></script>
<button onclick="code()">Log in</button>
<div style="background:url('javascript:alert('xss')')">

Solution: disable inline JavaScript

external

inline

inline

inline

code

code

code

code

Attacker (probably*) cannot change JavaScript on our server

How to disable inline JavaScript: Content-Security-Policy (CSP)

<meta http-equiv="Content-Security-Policy" 
    content="script-src https://code.jquery.com 'self';">

Wide support for CSP (browsers, web apps)

use HTTP headers or HTML meta

Examples

// server
const app = koa()
app.use(function * () {
  this.render('index.jade', {
    greeting: 'Hi {{constructor.constructor(\'alert(1)\')()}}'
  })
})
// Jade template
doctype html
html(lang="en")
  head
    meta(http-equiv="Content-Security-Policy",
      content="script-src https://code.angularjs.org 'self';")
    script(src="https://code.angularjs.org/1.0.1/angular-1.0.1.js")
    title Example
  body(ng-app)
    h1 #{greeting}

Back to our example

Added

// server
const app = koa()
app.use(function * () {
  this.render('index.jade', {
    greeting: 'Hi {{40 + 2}}'
  })
})

Regular Angular expressions work with CSP

// server
const app = koa()
const helmet = require('koa-helmet')
app.use(helmet.csp({
  directives: {
    defaultSrc: [],
    scriptSrc: ['https://code.angularjs.org'],
    styleSrc: [],
    imgSrc: [],
    objectSrc: [],
    sandbox: ['allow-scripts']
  }
}))
// $ http localhost:4001
// HTTP/1.1 200 OK
// Content-Security-Policy: default-src; \
//   script-src https://code.angularjs.org; \
//   style-src; img-src; object-src; sandbox allow-scripts

Use response headers for CSP

CSP info: content-security-policy.com

validation, checking: cspvalidator.org

Server-side templates 😟

app.get('/', function (req, reqs) {
  res.render('index', {
    title: 'Example',
    analyticsId: '4xy-0123456'
  });
});
head
  title #{ title }
  script.
    var analyticsId = '#{ analyticsId }';
  script.
    // use variable analyticsId

inline

Content-Security-Policy: 
  script-src 'unsafe-inline'

You can add exceptions to CSP

Content-Security-Policy: 
  script-src 'nonce-2726c7f26c'
<script nonce="2726c7f26c">
  var inline = 1;
</script>

You can add exceptions to CSP

Content-Security-Policy: 
  script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='
<script>
  var inline = 1;
</script>

Just keep adding hashes

Solution: render external JS

// analytics-config.js
var analyticsId = '4xy-0123456';
head
  meta(http-equiv="Content-Security-Policy",
    content="script-src 'self';")
  title #{ title }
  script(src="js/analytics-config.js")
  script(src="js/analytics.js")

js-to-js middleware

npm install --save js-to-js

Use as Express.js middleware

Save inline scripts as JavaScript files

Generate external JavaScript with values on the fly

js-to-js middleware

// views/config.js
module.exports = {
  analyticsId: 'default-id'
}
// server.js
var app = require('express')()
var jsToJs = require('js-to-js')
app.engine('js', jsToJs)
app.get('/js/analytics-config.js', function (req, res) {
  res.setHeader('content-type', 'application/javascript');
  res.render('config.js', {
    analyticsId: '4xx-xxxxx'
  });
});

actual value

js-to-js middleware

// js/analytics-config.js
var analyticsConfig = {
  "analyticsId": "4xx-xxxxx"
};

User receives

head
  meta(http-equiv="Content-Security-Policy",
    content="script-src 'self';")
  title #{ title }
  script(src="js/analytics-config.js")
  script(src="js/analytics.js")

Pass entire config at once

var jsToJs = require('js-to-js');
app.get('/js/demo-config.js',
  jsToJs('demoConfig', { foo: 42, bar: 21 }));

User receives

// js/demo-config.js
var demoConfig = { foo: 42, bar: 21};

generates script with object "window.demoConfig"

js-to-js also wraps JS scripts (like Google Analytics)

js-to-js middleware

  1. Wraps your inline JS code

  2. Wraps config objects (shortcut)

  3. Wraps 3rd party scripts that need config

With a little help, your backend server can send ZERO inline JavaScript

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.com;

this site is no longer an easy target

It is free!

Conclusions

Do not confuse code with data

Disable all inline JavaScript

Do not be an easy target

When I open a browser ...

Gleb Bahmutov

Thank you 👏

Securing the front end, from a Node server

By Gleb Bahmutov

Securing the front end, from a Node server

A simple technique to disable the script injection attacks on your web pages is to disable the inline JavaScript. This means that most popular ways to inject variables and code fragments into your pages will have to change. I will show the JavaScript to JavaScript rendering engine for Express that allows you to set very strict and safe Content-Security-Policy on your website. Not only my approach is much safer, but it will be very testable as well.

  • 3,137