#boscc
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?
curl -I https://github.com/
$ 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:
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
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 ">"
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 content
User comments
Ads, etc
<script>stealStuff()</script>
source: www.brit.co/latte-art/
// 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><script>alert("1");</script></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
Angular 1.x edition
{{constructor.constructor('alert(1)')()}}
fixed in Angular 1.2
{{ (_=''.sub).call.call({}[$='constructor'].getOwnPropertyDescriptor ( _.__proto__,$).value,0,'alert(1)')() }}
{{ 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;'' }}
{{ '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
<body>
<script>function willNotRun() {...}</script>
<script src="assets/js/script.js"></script>
</body>
external
inline
<meta http-equiv="Content-Security-Policy"
content="script-src https://code.jquery.com 'self';">
Wide support for CSP (browsers, web apps)
// 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
CSP info: content-security-policy.com
validation, checking: cspvalidator.org
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
// 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")
// 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/config.js
var analyticsConfig = {
"analyticsId": "4xx-xxxxx"
};
User receives
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};
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
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
NPM auth token, GitHub tokens, passwords, SSH keys, etc
#boscc