Data Security 4 - OWASP Top Ten - SQL Injections
References for this Part
OWASP Top Ten 2025
Model Solutions Previous Lesson
Example Model DS.3.0 register.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// register.js
const bcrypt = require('bcryptjs');
const sqlite3 = require('better-sqlite3');
const Rockyou = require('./Rockyou.js');
const argv = process.argv;
const argc = argv.length;
const db = new sqlite3('test.db'); // includes connect
const register = function(user, pwd) {
let hash = bcrypt.hashSync(pwd);
try {
let sql = 'insert into user(userid, password) values(?, ?)';
let stmt = db.prepare(sql);
stmt.run(user, hash);
return true;
} catch(err) {
console.log(`${err.message}`);
return false;
}
}
let entereduser = argv[2];
let enteredpassword = argv[3];
let rockyou = new Rockyou();
let exists = rockyou.search(enteredpassword);
if (!exists) {
let res = register(entereduser, enteredpassword);
if (!res) {
console.log('db error');
} else {
console.log('You chose wisely');
}
} else {
console.log('inadequate password');
}
db.close();
|
Example Model DS.3.0 register.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
# register.py
# https://docs.python.org/3/library/sqlite3.html for doc
# https://www.slingacademy.com/article/python-sqlite3-insert-new-row-get-id/
import bcrypt
import rock
import sqlite3
import sys
def register(user, pwd):
con = sqlite3.connect('test.db')
cur = con.cursor()
salt = bcrypt.gensalt()
hash = bcrypt.hashpw(pwd, salt)
try:
res = cur.execute('insert into user (id, userid, password) values(null, ?, ?)', (user, hash))
con.commit()
return True
except sqlite3.Error as err:
print(err.sqlite_errorcode, err.sqlite_errorname)
return False
if __name__ == '__main__':
argv = sys.argv
entereduser = argv[1]
enteredpassword = argv[2]
rockyou = rock.Rockyou()
exists = rockyou.search(enteredpassword)
if not exists:
enteredpassword = enteredpassword.encode()
result = register(entereduser, enteredpassword)
if not result:
print('db error')
else:
print('inserted')
else:
print('inadequate password')
|
OWASP
The Open Worldwide Application Security Project (OWASP) is a nonprofit foundation that works to improve the security of software. Our programming includes:
- Community-led open source projects including code, documentation, and standards
- Over 250+ local chapters worldwide
- Tens of thousands of members
- Industry-leading educational and training conferences
Injections - Create a Situation
An obvious target for the hacker in order to try to gain unauthorized
access to a system is the login screen. An alarming number of real
life login situations are not well protected. We shall look at some
issues to illustrate, and we shall see what we, as developers, can
do to protect our applications.
The core of any web application is the router. Let us look at an example:
Example 1: The Router, vulnerable0/routes/router.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
'use strict';
/*
* check if routed handler function exists
* if yes call it, else complain
*/
const handlers = require('../controllers/handlers'); // handlers module
const requestHandlers = { // application urls here
GET: {
'/': handlers.home,
'/page1': handlers.other,
'/about': handlers.other,
'/contact': handlers.other,
'/contacts': handlers.contacts,
'/login': handlers.login,
'/logout': handlers.logout,
'/notfound': handlers.notfound,
'js': handlers.js,
'css': handlers.css,
'png': handlers.png,
'svg': handlers.svg,
'ico': handlers.ico
},
POST: {
'/contact': handlers.receiveContacts,
'/login': handlers.verifyLogin
}
}
module.exports = {
route(req, res, bodydata) {
let urls = req.url.split('?'); // separate query string from url
req.url = urls[0]; // clean url
let arr = req.url.split('.'); // filename with suffix
let ext = arr[arr.length - 1]; // get suffix
// look for route
if (typeof requestHandlers[req.method][req.url] === 'function') {
// if found, call
requestHandlers[req.method][req.url](req, res, bodydata);
} else if (typeof requestHandlers[req.method][ext] === 'function') {
// if css, js, png, or ... get it
requestHandlers[req.method][ext](req, res);
} else {
requestHandlers.GET['/notfound'](req, res);
// use notfound
}
}
}
|
When we submit the login form we are issuing a POST request to
the server. Please look at lines 25, and 35-36 above. The router
will accordingly send us to the verifyLogin function in the
following requesthandler code. This can easily be verified by
displaying the source code of the form in your browser.
Example 2. The Handlers, vulnerable0/controllers/handlers.js
, (fragment)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
'use strict';
/*
* handlers.js
* Requesthandlers to be called by the router mechanism
*/
// ...
const models = require("../models/dbCode"); // models are datahandlers
// ...
async verifyLogin (req, res, data) {
let obj = lib.makeWebArrays(req, data) // home made GET and POST objects
let r = await models.verify(obj);
console.log(`read for login-verify: ${r.length} = number read`);
if (r.length > 0) {
// anybody read
cookie.set(req, res, '' + r[0].name); // create login cookie
res.writeHead(httpStatus.MOVED_TEMPORARILY, { // write header
"Location": '/'
});
res.end(); // send it
} else {
res.writeHead(httpStatus.MOVED_TEMPORARILY, { // write header
"Location": '/logout'
});
res.end(); // send it
}
},
// ...
|
Example 3. The Model, vulnerable0/models/dbCode.js
(fragment)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
e strict';
/*
* models
* handlers for data manipulation
*/
const maria = require("mariadb"); // file system access
const dbp = require('./dbParams.js');
const crypto = require('crypto');
module.exports = {
//...
async verify(obj) {
const dbh = await maria.createConnection(dbp);
let email = obj.POST.email;
console.log(`før sql: ${email} ${obj.POST.password}`);
let pwd = await crypto.createHash('sha512')
.update(obj.POST.password, 'utf8')
.digest('hex');
let sql = `select name, password
from user
where email = '${email}' and password = '${pwd}'`;
console.log(`sql stmt: ${sql}`);
let rows = await dbh.query(sql);
return rows;
}
}
|
This code connects to the database server, tries to read
from the database based on the entered credentials, and
returns the result to the controller that invoked it.
In order to verify a hashed password, we must hash the
entered clear text password with the algorithm used to
hash passwords in the system, and if the two hashes
are equivalent, and the user ids match too, login
is registered by issuing a cookie as a token.
Injections - Demo 0, SQL Injection
In the previous section we outlined an application where the
two pertinent functionalities are user registration, and login.
Pretend that the user registration is done securely, and the
user has altered the password to a secret one.
We shall first demonstrate that required functionalities are
in place. We start the server, and access it by linking to
http://localhost:3000 in the browser. Then we shall
- Create a user,
- login as that user, and
- verify the privilege(s) it entails
We have verified that something actually works. Let us
verify that the database actually holds a hashed password,
and thus meets the cornerstone of all user security that
passwords are never stored as clear text.
Example 4. The Demo Database, vulnerable0
1
2
3
4
5
6
7
8
9
|
select * from user;
+-------+---------------+----------+----------------------------------------------------------------------------------------------------------------------------------+--------------+---------+
| name | email | phone | password | bio | profile |
+-------+---------------+----------+----------------------------------------------------------------------------------------------------------------------------------+--------------+---------+
| zebra | abekat@nml.dk | 12345678 | y | jaaah | 0 |
| jane | jane@doe.com | 1236 | x | regular | 0 |
| john | john@doe.com | 1235 | x | regular | 0 |
| nmla | nmla@iba.dk | 1234 | ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff | Hi I'm admin | 1 |
+-------+---------------+----------+----------------------------------------------------------------------------------------------------------------------------------+--------------+---------+
|
The hashes, done with SHA512, are 512 bits long, 64 bytes,
and stored as hexidecimal digits amounting to 128 characters.
This is pretty normal. We use blob
as the attribute data type.
This is useful because it means we do not have to change type
if we update to a longer hash size.
Injections - Discussion of Demo 0
We verified that it works. We saw the database content.
What can possibly go wrong? The short answer to this rhetorical
question is everything!
There are in the above at least three obvious vulnerabilities that
we must discuss, and deal with.
- The login process is easily hackable.
- The hashing of the passwords is not optimal.
- User form input is not sanitized.
We shall illustrate the problems and discuss
countermeasures.
Challenging the Login
Let us look at the model code that responds to the login.
We have shown it in Example 3, let us repeat lines 21
through 23 for easier reference:
Example 5. Fragment of a Fragment from Example 3
1
2
3
4
5
6
7
|
// ...
// email is from the html form
// pwd is a hash of the password from the html form
let sql = `select name, password
from user
where email = '${email}' and password = '${pwd}'`;
// ...
|
The practical demo could show a result of a login
with nmla@iba.dk
and test
being:
Example 6. Resulting sql
Statement
1
2
3
|
select name, password
from user
where email = 'nmla@iba.dk' and password = 'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff'
|
meaning that the user input is taken as is, no questions asked.
That is bad. The above sentence can be used directly in
an interactive session with mariadb
:
Example 7. Try in native mariadb
1
2
3
4
5
6
7
|
MariaDB [vulnerable0]> select name, password from user where email = 'nmla@iba.dk' and password = 'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff';
+------+----------------------------------------------------------------------------------------------------------------------------------+
| name | password |
+------+----------------------------------------------------------------------------------------------------------------------------------+
| nmla | ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff |
+------+----------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.000 sec)
|
Everything looks fine and innocent.
Now enter the read team. Given the login form
they will enter
Example 8. Red Team Action
1
2
|
Userid: ' or 1=1; --
Password: whatever cats footprint
|
resulting in
Example 9. Injected sql
Statement
1
2
3
|
select name, password
from user
where email = '' or 1=1; -- ' and password = '1199088afb557c65bfc348ff6b8522a8fcc19d9f3783d778568c66a61ba05921aefa07384a3899685fe9f51563ab67b54dae5fcb937ed5b8292e1cad9470ac04'
|
Example 10. The Injected Statement in mariadb
1
2
3
4
5
6
7
8
9
10
11
12
|
MariaDB [vulnerable0]> select name, password
-> from user
-> where email = '' or 1=1; -- ' and password = '1199088afb557c65bfc348ff6b8522a8fcc19d9f3783d778568c66a61ba05921aefa07384a3899685fe9f51563ab67b54dae5fcb937ed5b8292e1cad9470ac04'
+-------+----------------------------------------------------------------------------------------------------------------------------------+
| name | password |
+-------+----------------------------------------------------------------------------------------------------------------------------------+
| zebra | y |
| jane | x |
| john | x |
| nmla | ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff |
+-------+----------------------------------------------------------------------------------------------------------------------------------+
4 rows in set (0.000 sec)
|
Alternatively, it is better for red team to enter as an
administrator, and if they know that nmla@iba.dk is the
administrator, they might try that in the user field as
nmla@iba.dk'; --
and with whatever in the password field resulting in
1
2
3
|
let sql = `select name, password
from user
where email = 'nmla@iba.dk'; -- ' and password = 'whatever'`;
|
In both cases the injection allows the red team to gain
access bypassing the password requirement.
A successful read, a successful login. This is the quintessential
sql injection example.
The issue here is that red team uses the '
, apostrophe, the sql
string delimiter, to intersect the declaration logic, and then
the ;
, semicolon, to end the declaration and the --
,
dash, dash, space, to leave the rest as an sql comment.
In other words building an sql declaration with unsanitized text
input from the user is a risk. The metacharacters from sql should
not be allowed in critical user input.
Sanitizing may be done in the client, or the server, or both.
The client alone is insufficient. The red team may easily replace
the HTML5 form with one of their own making. Doing it client side
is optimal as it will result in speedy error messages to the
ordinary user. Minimally it must be done server side.
The server side must either escape all critical characters,
or use prepared statements for the sql declarations.
The latter is best practice, it implies escaping, and is a must.
Optimizing the Hashing
If the evil hacker should see the above he would notice that,
of course, and in a few seconds he would also notice that
users anat, and user tulle have equal hashed passwords. This
means that the passwords have probably not been salted
while being hashed. At least not with individually
randomized salts. Apart from the fact that cracking one
users password gives away other users with the same
password, this also means that the time consumption for
cracking the passwords is shorter. In other words,
we have helped the hacker.
Salting means, as discussed earlier, adding a random element
to the hashing process so that cracking will be more time
consuming and harder. It also means that the same password
will hash differently in different hashing processes.
This entails that the salt must be stored alongside
the hash, otherwise it would be impossible to verify
passwords in the login process. It also renders the
sql code in example 9 useless.
On login the application takes the entered password,
hashes it, and compares it to the stored
hashed password. If they match, login is succesful.
But, if salting the entered password is done with
random salt, there is no way we can create the same
hash in the login process. This problem is not
insurmountable, we must change the code so that
we first read the password and the salt, and then
do the verification based on the read salt.
A subtle difference that does not allow the sql
select to do the verification.
Sanitizing
We shall not dive into that at this time. It is common
development practice, but we must remember to do it at
each tier of the client server model.
Using Prepared Statements
Using prepared statements is also, or should also
be common development practice. We shall show you
a simple example.
We shall show you two fragments of examples 12 and 13
the previous lesson:
Prepared Statement in sql
from Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# insert into the database table - neccessary in register
sql = "insert into user(id, userid, password) values (null, ?, ?)"
res = cur.execute(sql, (argv[1], 'x'))
con.commit()
# select all rows from table
sql = 'select * from user'
res = cur.execute(sql)
print(res)
# select one from table - neccessary before compare/verify
sql = 'select userid, password from user where userid = ?'
res = cur.execute(sql, (argv[1]))
print(res);
|
Prepared Statement in sql
from JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// insert into the database table - neccessary in register
sql = "INSERT INTO user(userid, password) VALUES (?, ?)";
stmt = db.prepare(sql);
res = stmt.run(argv[2], 'x');
console.log(res);
// select all rows from table
sql = 'select * from user';
stmt = db.prepare(sql);
rows = stmt.all();
console.log(rows);
// select one from table - neccessary before compare/verify
sql = 'select userid, password from user where userid = ?';
stmt = db.prepare(sql);
rows = stmt.get(argv[2]);
console.log(`${rows.userid}, password=${rows.password}`);
|
Exercises
The rules for handing in assignments may be found in the
README
Documentation in the following exercises MUST be written
into the README of the exercise as a description of what
to do, and what results to expect.
Exercise DS.4.0
Test your solution to exercise
In Exercise DS.3.0
You built a registration program.
Now you must build a login mechanism using the
user
table from the database.
Let the program respond by printing id
, and userid
if the login succeeds, and error
if it fails.
Verify and document that the program is immune to
sql injection.
Exercise DS.4.1
Create a new version of your solution to
Exercise DS.3.0
where you replace the use of bcrypt
with sha256
.
You SHOULD create a user256
table in that database to
avoid creating a second database.
1
2
3
4
5
6
|
CREATE TABLE user256 (
id integer primary key autoincrement,
userid varchar(32) unique not null,
password blob nut null,
comments text not null
);
|
Please notice that there is a comments
column.
Do not omit it. It is for later use.
Then build a login mechanism using this database table.
Let the program respond by printing id
, and userid
if the login succeeds, and error
if it fails.
Verify and document that the program is immune to
sql injection.