Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
aka "Dr Gleb"
Conferences: ng-conf, AngularConnect 2016, etc.
58 blog posts tagged "angularjs"
@bahmutov
- "Unit testing AngularJS code in record time"
- "Stop (silent) Angular (name) overrides"
- "Improving Angular web app performance"
@bahmutov
A DATA ANALYTICS AND MACHINE INTELLIGENCE COMPANY
A DATA ANALYTICS AND MACHINE INTELLIGENCE COMPANY
What happens to the value of X when Y happens?
@bahmutov
@bahmutov
Source: Uruk (modern Iraq)
time
today
2013
future
Angular 1.0
Angular 1.x
Vue.js
Aurelia
Angular 2
OWASP top 10
Server-side
rendering
No more security?
1985
Security is compromised when there is mismatch of expectations between two parts of the system
@bahmutov
Database code assumes user email has been validated and sanitized
sqlCommand('SELECT * FROM USERS WHERE email=' + email)
@bahmutov
Website code assumes database code checks the email in secure fashion
sqlCommand('SELECT * FROM USERS WHERE email=' + email)
https://mysite.com/user?email=joe@mysite.com or 1=1
https://mysite.com/user?email=joe@mysite.com
Mismatch of expectations between the website and database systems
// the input data has been sanitized and can be trusted
// I expect SQL library to guard against invalid inputs
Mobile test + crash + App Store data = profit
@bahmutov
performance, crashes
performance - 2 stars
reliability - 1 star
ui features - 0.5 stars
....
@bahmutov
@bahmutov
@bahmutov
mockups
website
Balsamiq mockup https://balsamiq.com/products/mockups/
Balsamiq mockup https://balsamiq.com/products/mockups/
Balsamiq mockup https://balsamiq.com/products/mockups/
Lorem ipsum dolor sit amet, esse deseruisse dissentias et duo, in tantas putant urbanitas nam. Per dicant copiosae ea, duo ex omnesque copiosae. In rebum zril integre mea, an zril ornatus disputando pri. Mei at laudem ceteros, vidisse electram intellegam mel an, amet consul qui eu. Vix id melius consetetur, nam ea congue legimus oportere. Vim ne fastidii volutpat salutatus, nulla ludus at duo.
@bahmutov
@bahmutov
saturation
grey
red
@bahmutov
value
dark
light
@bahmutov
@bahmutov
Lorem ipsum dolor sit amet, esse deseruisse dissentias et duo, in tantas putant urbanitas nam. Per dicant copiosae ea, duo ex omnesque copiosae. In rebum zril integre mea, an zril ornatus disputando pri. Mei at laudem ceteros, vidisse electram intellegam mel an, amet consul qui eu. Vix id melius consetetur, nam ea congue legimus oportere. Vim ne fastidii volutpat salutatus, nulla ludus at duo.
This background?
@bahmutov
Lorem ipsum dolor sit amet, esse deseruisse dissentias et duo, in tantas putant urbanitas nam. Per dicant copiosae ea, duo ex omnesque copiosae. In rebum zril integre mea, an zril ornatus disputando pri. Mei at laudem ceteros, vidisse electram intellegam mel an, amet consul qui eu. Vix id melius consetetur, nam ea congue legimus oportere. Vim ne fastidii volutpat salutatus, nulla ludus at duo.
Or this background?
@bahmutov
Lorem ipsum dolor sit amet, esse deseruisse dissentias et duo, in tantas putant urbanitas nam. Per dicant copiosae ea, duo ex omnesque copiosae. In rebum zril integre mea, an zril ornatus disputando pri. Mei at laudem ceteros, vidisse electram intellegam mel an, amet consul qui eu. Vix id melius consetetur, nam ea congue legimus oportere. Vim ne fastidii volutpat salutatus, nulla ludus at duo.
#1e1e1e
#222222
@bahmutov
color swatch
AngularConnect with custom colors
@bahmutov
@bahmutov
<!-- assumes jQuery / Bootstrap / Angular 1 -->
<link rel="stylesheet" href="color-pusher/dist/color-pusher.css">
<script src="color-pusher/dist/color-pusher.js"></script>
// if DEBUG_SERVER
<div ng-app="color-pusher">
<color-pusher></color-pusher>
</div>
// endif
@bahmutov
load color-pusher via code snippet
If you do not control the page source
color-pusher demo
@bahmutov
@bahmutov
@bahmutov
@bahmutov
color-pusher DevTools panel
@bahmutov
Panels can be inspected just like other web apps in DevTools
@bahmutov
<link href="libs/bootstrap.min.css" rel="stylesheet" />
<link href="libs/color-pusher.css" rel="stylesheet" />
<link href="libs/angular-csp.css" rel="stylesheet" />
<link href="color-pusher-panel.css" rel="stylesheet" />
<script src="libs/jquery.min.js"></script>
<script src="libs/bootstrap.min.js"></script>
<script src="libs/angular.js"></script>
<script src="libs/color-pusher.js"></script>
<script src="color-pusher-panel.js"></script>
<link href="https://maxcdn.../font-awesome.min.css"
rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.1.0.min.js">
</script>
@bahmutov
Currently, we allow whitelisting origins with the following schemes: blob, filesystem, https, chrome-extension, and chrome-extension-resource.
@bahmutov
Whitelisting good behavior works. Blacklisting all possible bad behavior does not.
@bahmutov
google.com
hackme.com
easymoney.ru
bitcoinz.cn
mybank.ro
netflx.com
...
@bahmutov
@bahmutov
No data from JSONP http
<script src="http://foo...?callback=JSONP">
</script>
var url = 'http://www.colourlovers.com/api/palette/'
+ $scope.paletteId
var options = {
url: url,
params: {
format: 'json',
jsonCallback: 'JSON_CALLBACK'
}
}
$http.jsonp(url, options)
JSONP({results: 'foo', ...})
@bahmutov
@bahmutov
source: http://www.summitpost.org/
JSONP
AJAX
WWW
CORS
HTTPS everywhere
W3C standards
ECMA (JavaScript)
URI and HTTP (IETF)
1989
2000
2004
2006
@bahmutov
https://www.nostarch.com/tangledweb.htm
@bahmutov
<!DOCTYPE html>
<html ng-csp ng-app="color-pusher-panel">
...
</html>
@bahmutov
<script>
alert('foo')
</script>
<button onclick="alert('foo')">Click</button>
@bahmutov
<script src="lib/foo.js"></script>
<script src="https://whatever.com/foo.js"></script>
@bahmutov
<style>
h1 { color: red }
</style>
<button style="color: red">Click</button>
@bahmutov
<link rel="stylesheet" href="lib/app.css">
<link rel="stylesheet"
href="https://whatever.com/app.css">
@bahmutov
Chrome Extension DevTools Panel runs in Content Security Policy (CSP) mode
Angular 1 uses inline styles and eval
By default extension CSP only allows external scripts and styles
@bahmutov
Angular 1 creates inline style tag
@bahmutov
<!DOCTYPE html>
<html ng-csp ng-app="color-pusher-panel">
<link href="libs/angular-csp.css" rel="stylesheet" />
<script src="libs/angular.js"></script>
</html>
Note: ng-csp mode can make $parse service 30% slower
no eval used
@bahmutov
Boston, September 2016
@bahmutov
What if a tree's password is stolen in the forest and no one hears it?
@bahmutov
You can have your
website hacked but only once.
@bahmutov
Top 10 security threats
@bahmutov
@bahmutov
Who are you again?
An attacker impersonating a legitimate user can do a lot of damage
@bahmutov
User authentication token is sent via insecure connection
Use https* for every communication link
@bahmutov
@bahmutov
nginx config from https://weakdh.org/sysadmin.html
# put strong ciphers first # ban weak ciphers at the end ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:!CAMELLIA:!DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
@bahmutov
http://bahmutov.github.io
https://glebbahmutov.com
GitHub pages
CloudFlare
@bahmutov
Auth cookie not marked "httpOnly" and "secure"
cookie cannot be accessed by JavaScript code
cookie only included with https requests
+ "SameSite"
@bahmutov
@bahmutov
Is this code or data?
Attacker's input is treated as trusted code and is executed when another user views the website
@bahmutov
Name:
John Doe
<span>{{ name }}<span>
<span>John Doe<span>
@bahmutov
Name:
<script>alert('hi')</script>
<span>{{ name }}<span>
<span><script>alert('hi')</script><span>
innerText or innerHTML?
@bahmutov
innerText or innerHTML?
Direct text or escaped? ">" or ">"
@bahmutov
innerText or innerHTML?
Dangerous, but has styling, elements, etc
Really dangerous if one user's content
can be viewed by another user
@bahmutov
My content
User comments
Ads, etc
<script>stealStuff()</script>
@bahmutov
Will a script embedded in the user-entered content be executed (for another user)?
NEVER EVER EVER?
@bahmutov
source: www.brit.co/latte-art/
@bahmutov
@bahmutov
// 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}
@bahmutov
<body>
<h1>Hello world</h1>
</body>
@bahmutov
// 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
@bahmutov
<body> <h1><script>alert("1");</script></h1> </body>
@bahmutov
// 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}
@bahmutov
<body> <h1>{{ a + b }}</h1> </body>
@bahmutov
// 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
@bahmutov
<body ng-app="" class="ng-scope"> <h1 class="ng-binding">0</h1> </body>
@bahmutov
"AngularJS better not execute {{ <script>...</script> }} tags or I will loose it!"
@bahmutov
AngularJS does NOT execute {{ <script>...</script> }} tags
@bahmutov
// 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}
@bahmutov
@bahmutov
Template is NOT in DOM
@bahmutov
Runs on every digest cycle
@bahmutov
{{ eval('alert(42)') }}
// does not work
{{ Function('alert(42)')() }}
// does not work
// this $scope.Function...
@bahmutov
var f = {}
f.constructor
// function Object(){[native code]}
f.constructor.constructor
// function Function(){[native code]}
Need to get "Function" somehow
{{constructor.constructor('alert(1)')()}}
@bahmutov
Angular 1.0 edition
{{constructor.constructor('alert(1)')()}}
fixed in Angular 1.2
@bahmutov
@bahmutov
Blacklisting
@bahmutov
// Sandboxing Angular Expressions
// ------------------------------
// Angular expressions are generally considered safe because these expressions only have direct
// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by
// obtaining a reference to native JS functions such as the Function constructor.
//
// As an example, consider the following Angular expression:
//
// {}.toString.constructor(alert("evil JS code"))
//
// We want to prevent this type of access. For the sake of performance, during the lexing phase we
// disallow any "dotted" access to any member named "constructor".
//
// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor
// while evaluating the expression, which is a stronger but more expensive test. Since reflective
// calls are expensive anyway, this is not such a big deal compared to static dereferencing.
//
// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
// against the expression language, but not to prevent exploits that were enabled by exposing
// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good
// practice and therefore we are not even trying to protect against interaction with an object
// explicitly exposed in this way.
//
// A developer could foil the name check by aliasing the Function constructor under a different
// name on the scope.
//
// In general, it is not possible to access a Window object from an angular expression unless a
// window or some DOM object that has a reference to window is published onto a Scope.
Angular expressions are generally considered safe
@bahmutov
{{ (_=''.sub).call.call({}[$='constructor'].getOwnPropertyDescriptor ( _.__proto__,$).value,0,'alert(1)')() }}
continued
works in Angular 1.2
@bahmutov
{{ 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;'' }}
for Angular 1.3
@bahmutov
{{ '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); }}
for Angular 1.4
@bahmutov
Searching for sandbox escapes
@bahmutov
Udemy, Microsoft Support 1.4.9
Indiegogo 1.4.8
Google Insights 1.3.15
JetBlue, Ford 1.2.8
Viacom 1.0.8
@bahmutov
The double mustaches interprets the data as plain text, not HTML. In order to output real HTML, you will need to use triple mustaches
constructor.constructor escape works
{{
vue.$el.insertAdjacentHTML('afterend',
'<script>alert(1)</script>')
}}
constructor.constructor escape works
@bahmutov
The Angular expression sandbox will be removed from Angular from 1.6 onwards, making the code faster, smaller and easier to maintain
@bahmutov
the existence of the sandbox only made some developers incorrectly believe that the expression sandbox protected them [from XSS attacks]
@bahmutov
Angular templates are the same as executable code
constructor.constructor escape works
@bahmutov
@bahmutov
<body>
<script>function willNotRun() {...}</script>
<script src="assets/js/script.js"></script>
</body>
external
inline
@bahmutov
@bahmutov
Content-Security-Policy:
default-src 'self';
script-src 'self' ajax.googleapis.com/angularjs/1.5.8/
Wide support for CSP (browsers, web apps)
@bahmutov
$ 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
<meta http-equiv="Content-Security-Policy"
content="script-src 'self'; ....">
Wide support for CSP (browsers, web apps)
@bahmutov
// 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
@bahmutov
@bahmutov
// server
const app = koa()
app.use(function * () {
this.render('index.jade', {
greeting: 'Hi {{40 + 2}}'
})
})
Regular Angular expressions work with CSP
@bahmutov
// 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
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
Problems: Server-side templates:
// 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")
@bahmutov
// 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
@bahmutov
// js/config.js
var analyticsConfig = {
"analyticsId": "4xx-xxxxx"
};
User receives
@bahmutov
@bahmutov
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};
@bahmutov
@bahmutov
CSP Is Dead, Long Live CSP!
On the Insecurity of
Whitelists and the Future of Content Security Policy
@bahmutov
@bahmutov
@bahmutov
new Worker(URL.createObjectURL(blob))
uses CSP of the parent document
new Worker('worker.js')
uses CSP in Content-Security-Policy HTTP header
@bahmutov
Bad news: this is CSP level 2 spec
NOT supported by IE / Edge
Whitelist allowed worker locations using 'child-src' CSP property
@bahmutov
@bahmutov
XSS into ServiceWorker via JSONP :)
Discussed in this Chromium thread
https://bugs.chromium.org/p/chromium/issues/detail?id=422966
@bahmutov
Just add one more library
@bahmutov
@bahmutov
@bahmutov
We trust this library
@bahmutov
Be sure CDN serves what you expect using Server Resource Integrity (SRI)
<script src="https://example.com/example-framework.js"
integrity="sha384-oqVuA..."
crossorigin="anonymous"></script>
Chrome & Firefox only http://caniuse.com/#feat=subresource-integrity
@bahmutov
A simple Node server example depends on > 100 modules
@bahmutov
webpack / browserify
loaders
gulp / grunt
build plugins
lint tools
jQuery
Angular
Bootstrap
App code
@bahmutov
webpack / browserify
jQuery
loaders
App code
Angular
gulp / grunt
Bootstrap
build plugins
lint tools
NPM checksums (like SRI) https://github.com/zaach/npm-seal
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
@bahmutov
What if someone unpublished negotiator@0.6.0 and then published malicious code under negotiator@0.6.0?
negotiator@0.6.1?
@bahmutov
@bahmutov
@bahmutov
* "fs" unpublished and restored: http://status.npmjs.org/incidents/dw8cr1lwxkcr
@bahmutov
Problem: Can someone publish under YOUR name?
@bahmutov
NPM auth token, GitHub tokens, passwords, SSH keys, etc
@bahmutov
@bahmutov
@bahmutov
Each public project: library, framework and application should a security page
@bahmutov
node / npm package
@bahmutov
(your) simple code is secure
Code is rarely simple
@bahmutov
function f() { alert('foo') }
@bahmutov
f``
@bahmutov
@bahmutov
Shai Reznik says
@bahmutov
f`` - popup
f - popup
``
f; - no popup
``
@bahmutov
@bahmutov
Mike Pennisi
When we started extending Test262 to cover brand new language features, we knew we were in for some surprises. Even so, we never could have anticipated the horrors we would uncover.
Mike Pennisi
... However, these language features can sometimes interact in strange ways. ... All of it is valid JavaScript, ... None of it would be accepted in a code review.
It is much harder to secure a system later
Crash reporting can warn you about CSP violations and JS probing
@bahmutov
Make yourself too expensive to hack for the value you protect!
@bahmutov
@bahmutov
slides.com/bahmutov/angular-connect-2016
By Gleb Bahmutov
What do Chrome extensions written in Angular and secure websites have in common? They take Content Security Policy seriously! In this presentation I will show how to secure your web application against cross-site scripting attacks and insecure 3rd party code.
JavaScript ninja, image processing expert, software quality fanatic