Cryptography alone does not solve the “CIA triad” of confidentiality, integrity and availability. SSL/TLS may stop attackers from intercepting connections. However, it doesn’t protect against attackers advantage of bugs in code.
As developers, it is our responsibility to write secure code. Writing secure code requires a security mindset that combines both paranoia and creativity. Paranoia, because you need to expect the very worst from malicious users in every line of code you write. Creativity, because you need to be imagining unexpected abuses of your system, long before any attackers dream up those attacks themselves.
Security is difficult. Fortunately, there are many good guides to developing secure websites. I recommend the Open Web Application Security Project (OWASP). Among their many guides and instructional recommendations, is their regular “Top Ten” effort. The OWASP Top 10 is a consensus of security professionals of the ten most important threats to web application security. The current top 10 is as follows:
In the remainder of this section, I will discuss the most important of the OWASP top 10: injection.
Injection
Injection can occur in code that unnecessarily trusts user-supplied data.
Suppose we have an application that uses SQL to query a database. The following query is a representation of what might occur when ‘mike’ attempts to log in.
async function checkMike() {
let sql = `select id, name from accounts
where username = 'mike'
and password = 'asdf1234'`;
return await pool.query(sql);
}
Of course, we do not hard-code a method for each user. Instead, the username and password are parameters supplied from user input. A naïve version of the login code might look like the following:
// WARNING: This code has problems!
async function checkLogin(username, password) {
let sql = `select id, name from accounts
where username = '${ username }'
and password = '${ password }'`;
return await pool.query(sql);
}
The above code will work, but it has a problem.
The issue is that the user can ‘inject’ SQL into the query.
For example, what happens if the user logs in with username mike
and password ' or '1' = '1
?
Take the query:
let sql = `select id, name from accounts
where username = '${ username }'
and password = '${ password }'`;
Replacing username and password, we would have:
let sql = `select id, name from accounts
where username = '${ "mike" " }'
and password = '${ "' or '1' = '1'" }'`;
The value of sql
would be the following:
select id, name from accounts
where username = 'mike'
and password = '' or '1' = '1'
Notice the or
in the last line? '1' = '1'
is always true! This means that the system will always log in (because anything or '1' = '1'
is always true).
Allowing users to log in without a valid password is a huge security issue.
Most database query libraries offer parameterized queries to solve this problem. Instead of building queries out of strings, a parameterized query allows you to write a query and supply the user input as separate parameters or values.
async function checkLogin(username, password) {
let sql = `select id, name from accounts
where username = $1
and password = $2`;
return await pool.query({
text: sql,
values: [username, password]
});
}
In this code, the database interprets the username and password as values. These values will not be decoded or executed as SQL code.