Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
https://lizkeogh.com/2019/07/02/off-the-charts/
+3 degrees Celsius will be the end.
If there is a company that fights global climate catastrophe and needs JavaScript and testing skills - I will do for free.
example: https://fab.earth
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)
15 years in development
MathWorks, Kensho, VP of Engineering at Cypress.io
100s of open source projects, blog posts mostly JavaScript
these slides
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>
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 ">"
innerText or innerHTML?
Dangerous, but has styling, elements, etc
Really dangerous if one user's content
can be viewed by another user
My content
User comments
Ads, etc
<script>stealStuff()</script>
Will a script embedded in the user-entered content be executed (for another user)?
// 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
<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')')">
external
inline
inline
inline
code
code
code
code
<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
// 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
Content-Security-Policy:
script-src 'unsafe-inline'
Content-Security-Policy:
script-src 'nonce-2726c7f26c'
<script nonce="2726c7f26c">
var inline = 1;
</script>
Content-Security-Policy:
script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='
<script>
var inline = 1;
</script>
// 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")
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
// 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/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")
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"
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.com;
this site is no longer an easy target
When I open a browser ...
By Gleb Bahmutov
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.
JavaScript ninja, image processing expert, software quality fanatic