Securing the front end

from a Node server

Gleb Bahmutov

#boscc

Who? Gleb @Bahmutov

Why? github.com/bahmutov

Where? Kensho.com

You can have your

website hacked but only once.

What if a tree's password is stolen in the forest and no one hears it?

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>

Name:

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

<span>{{ name }}<span>

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

innerText or innerHTML?

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

InnerHTML example

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

NEVER EVER EVER?

My Site

My content

 

 

 

 

 

 

 

 

User comments

Ads, etc

 

 

 

 

 

 

 

<script>stealStuff()</script>

Not to us!

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

Solution: disable inline JavaScript

<body>
  <script>function willNotRun() {...}</script>
  <script src="assets/js/script.js"></script>
</body>

external

inline

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)

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

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

// 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/config.js
var analyticsConfig = {
  "analyticsId": "4xx-xxxxx"
};

User receives

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

Pass entire config at once

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

User receives

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

With a little help, Express can send ZERO inline JavaScript

It is free!

Can an attacker change our server code?

It is a jungle out there

Simple server example depends on > 100 modules

jade-angular-template-attack@1.0.0
├─┬ koa@1.2.0
│ ├─┬ accepts@1.3.2
│ │ └── negotiator@0.6.0
│ ├── co@4.6.0
│ ├─┬ composition@2.3.0
│ │ └── any-promise@1.1.0
│ ├── content-disposition@0.5.1
│ ├── content-type@1.0.1
│ ├─┬ cookies@0.6.1
...
npm list

What if someone unpublished negotiator@0.6.0 and then published malicious code under negotiator@0.6.0?

negotiator@0.6.1?

bad-module@0.1.0

- is loaded by the Node system

- looks through require.cache

- starts spying on any "login" method

- steals stuff

Making "good" modules read-only is difficult

You can restrict APIs via preloader

$ npm i -g toolbag
$ node -r toolbag server.js

Do not let anyone publish under your name

please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please please

Kik-gate

  1. Guy has bunch of NPM modules including "kik" and "left-pad"
  2. Company Kik asks him to release "kik" name from NPM registry
  3. Guy refuses
  4. Kik goes to NPM Registry Co and Co gives "kik" name to Kik
  5. Guy gets angry and removes ALL his packages from NPM registry
  6. Bunch of packages depend on "left-pad" and stop working
  7. Guy #2 grabs released names AND PUBLISHES his code under same names and versions
  8. NPM Registry Co un-unpublishes original modules
  9. NPM Registry co changes its "unpublishing policy"

You cannot unpublish after 24 hours

Even unpublishing after 1 second is a RISK

If you push sensitive info to public GitHub or publish to NPM registry - it is out

All major NPM projects has some sensitive information leaked

NPM auth token, GitHub tokens, passwords, SSH keys, etc

Solution = prevention

ban-sensitive-files

Conclusions

Do not be an easy target

Do not be an easy target

Do not be an easy target

Gleb Bahmutov

#boscc

Secure the front end from Node server - Boston Code Camp 25

By Gleb Bahmutov

Secure the front end from Node server - Boston Code Camp 25

This presentation will show how to lock down the front end JavaScript code using Content-Security-Policy. I will also show how to prevent sensitive files from being committed to your repos or NPM registry. Presented at Boston Code Camp 25 on April 2nd 2016.

  • 2,236
Loading comments...

More from Gleb Bahmutov