NodeJS Security

  • Locking down front end JavaScript

  • Prevent sensitive files from leaking to Git or NPM

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?

Dangerous, but has styling, elements, etc

Really dangerous if one user's content

can be viewed by another user

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>

Will never happen to me

Angular 1 edition (for Jeff)

Examples where we can embed script code into {{ }} template

{{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>

Attacker (probably) cannot change JavaScript on our server

Browser console test

var el = document.createElement('script');
el.innerText = 'alert("hi there");'
document.body.appendChild(el); // runs the code by default

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-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

Solution: render external JS

// analytics-config.js
var analyticsId = '4xy-0123456';
// analytics.js
// included after analytics-config.js
// uses variable analyticsId to init page analytics

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 express = require('express');
var app = 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'
  });
});

js-to-js middleware

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

User receives

Wrap JS scripts (like ga)

// views/config.js with a function
module.exports = function (options) {
  initAnalytics(options.userId);
};
// server.js
var express = require('express');
var app = express();
var jsToJs = require('js-to-js');
app.engine('js', jsToJs);
app.get('/js/config.js', function (req, res) {
  res.setHeader('content-type', 'application/javascript');
  res.render('config.js', {
    userId: userId
  });
});

Wrap JS scripts (like ga)

(function (options) {
  initAnalytics(options.userId);
}({ userId: 'xx-yy-bb' }));

User receives

Config middleware

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

Can an attacker change our server code?

All major NPM projects has some sensitive information leaked

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

Solutions:

ban-sensitive-files

Gleb @bahmutov

NodeJS for secure websites

By Gleb Bahmutov

NodeJS for secure websites

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

  • 1,429
Loading comments...

More from Gleb Bahmutov