Chapter 1. Injection Attacks
Injection vulnerabilities are the most prevalent and dangerous of web application vulnerabilities. Typically, an injection vulnerability manifests when application code sends untrusted user input to an interpreter as part of a command or query. Attackers exploit the vulnerability by crafting hostile data that tricks the interpreter into executing unintended commands or accessing data without proper authorization.
In this chapter, we review two prevailing mechanisms of injection attacks: command and database.
Command Injection
Using command injection, an attacker can execute arbitrary commands on the host operating system of a vulnerable application. This flaw gives enormous opportunities to an attacker, ranging from reading restricted file contents to installing malware with which the attacker can take full control of the server and host network.
Attack Mechanics
The child_process
core module enables Node developers to invoke underlying OS commands from the application code. Due to its name and simplicity of use, the child_process.exec
method is commonly used for making system calls.
The exec
method takes three arguments: a command in string format, an optional options object, and a callback function, as demonstrated in Example 1-1.
Example 1-1. child_process.exec command
child_process
.
exec
(
command
[,
options
][,
callback
])
Although the exec
method executes OS commands in a nonblocking manner, perfectly aligning with Node’s async programming paradigm, its flexibility to pass the command as a string often invites injection flaws. This is particularly the case when a user input is used to construct the command.
For example, Example 1-2 shows using the child_process.exec
method to invoke the gzip
command that appends a user-supplied dynamic file path to construct the gzip
command.
Example 1-2. Executing gzip command by using child_process.exec
child_process
.
exec
(
'gzip '
+
req
.
body
.
file_path
,
function
(
err
,
data
)
{
console
.
log
(
data
);
});
To exploit the injection vulnerability in the preceding code, an attacker can append ; rm -rf /
, for instance, to the file_path
input. This allows an attacker to break out of the gzip
command context and execute a malicious command that deletes all files on the server. As part of the user input, an attacker also can chain multiple commands by using characters such as ;
, &
, &&
, |
, ||
, $()
, <
, >
, and >>
.
The attack manifests because, under the hood, the exec
method spawns a new bin/sh
process and passes the command
argument for execution to this system shell. This is equivalent to opening a Bash interpreter for an attacker to run any commands with the same privileges as the vulnerable application.
Preventing Command Injection
Now that you know the potential of an injection attack to cause severe damage, let’s go over methods to prevent it.
Use execFile or spawn instead of exec
When possible, use the child_process
module’s execFile
or spawn
methods instead of exec
.
Unlike exec
, the spawn
and execFile
method signatures force developers to separate the command and its arguments.
The code in Example 1-3 demonstrates executing the gzip
command by using execFile
method.
Example 1-3. Executing the gzip command by using child_process.execFile
// Extract user input from request
var
file_path
=
req
.
body
.
file_path
;
// Execute gzip command
child_process
.
execFile
(
'gzip'
,
[
file_path
],
function
(
err
,
data
)
{
console
.
log
(
data
);
});
Any malicious commands chained to file_path
user input end up in execFile
method’s second argument of type array. Any malicious commands in user input are simply ignored or cause a syntax error if they’re not relevant to the target command, thus foiling the command injection attempts.
Input validation
Although execFile
or spawn
are safer alternatives to exec
, these methods cannot completely prevent command injection. The related scenarios include developers using these methods to invoke a custom script that processes user inputs, or to execute certain OS commands (such as find
, awk
, or sed
) that allow passing options to enable file read/write.
Just like other injection attacks, command injection is primarily possible due to insufficient input validation. To protect against it, verify that user-controlled command arguments and command options are valid.
Using a Whitelist Approach for Input Validation
When writing the validation logic, use a whitelist approach; that is, define what is permitted and reject any input that doesn’t fit this definition. Avoid writing this logic in an opposite manner (blacklist approach), or, in other words, by comparing input against a set of known unsafe characters. Attackers can often find a way to circumvent such filters by being creative with the input construction.
Using the Joi Module for Input Validation
The Joi
module provides a convenient and robust mechanism for input validation. It allows externalizing validations rules in a schema object and validating user inputs against it. These rules can verify the shape of the user input object, data type, and specific constraints on input values, as well as whitelist validation by using the any.valid()
API method.
Limit user privileges
If an attacker becomes successful at command injection, the injected command runs with the same OS-level privilege as the vulnerable application. By following the principle of least privilege, you can limit the attack surface of the command injection.
Specifically, the Node.js process should not run with root privileges. Instead, run it with a user that has access to only the required resources and no read–write access outside the web application directory.
Database Injection
With a successful database injection, an attacker can execute malicious commands on a database to steal sensitive data, tamper with stored data, execute database administration operations, access contents of files present on the database filesystem, and, in some cases, issue commands to the host operating system.
Let’s review the injection attacks on both SQL and NoSQL databases.
SQL Injection Attack Mechanics
Dynamic database queries that include user-supplied inputs are the primary target behind the SQL injection attack. When malicious data is concatenated to a SQL query, the SQL interpreter fails to distinguish between the intended command and input data, resulting in the execution of the malicious data as SQL commands.
Let’s consider a vulnerable SQL query, as shown in Example 1-4, that authenticates a user.
Example 1-4. A dynamically constructed SQL query that is vulnerable to SQL injection
connection
.
query
(
'SELECT * FROM accounts WHERE username ="'
+
req
.
body
.
username
+
'" AND password = "'
+
passwordHash
+
'"'
,
function
(
err
,
rows
,
fields
)
{
console
.
log
(
"Result = "
+
JSON
.
stringify
(
rows
));
});
Example 1-4 illustrates code that dynamically constructs a SQL query by appending the user-supplied request parameter username
. To exploit this query, an attacker can enter admin' --
as a username
, which ultimately results in executing the SQL query, as shown in Example 1-5.
Example 1-5. Resultant query with username as admin’ —
SELECT
*
FROM
accounts
WHERE
username
=
'admin'
The malicious user input eliminated the need for an attacker to submit a correct password because the part after --
in a SQL query is interpreted as a comment, thus skipping the password comparison.
Another, more destructive, variation of SQL injection is possible with databases that support batch execution of multiple statements when those statements are followed by a semicolon.
For example, if an attacker enters the string admin'; DELETE FROM accounts; --
as username
, the resultant query is equivalent to two statements, as shown in Example 1-6.
Example 1-6. Resultant query containing multiple SQL statements
SELECT
*
FROM
accounts
WHERE
username
=
'admin'
;
DELETE
FROM
accounts
;
This query results in removing all user accounts from the database.
An attacker can use a wide variety of malicious inputs for conducting injection, such as the following:
-
1' OR '1'='1
or its equivalent URL-encoded text1%27%20OR%20%271%27%20%3D%20%271
to get all records and ignore the latter part of theWHERE
clause -
%
in user input to match any substring or_
to match any character
Preventing SQL Injection
The good news is that SQL injection is easy to prevent by using a few simple measures.
Use parameterized queries to bind all user-supplied data
A parameterized query is considered a silver bullet for preventing the dreaded SQL injection. Parameterized queries prevent an attacker from changing the intent of a query, and enable the SQL interpreter to distinguish clearly between code and data. Hence, as shown in Example 1-7, always bind all user inputs to the query with parameters.
Example 1-7. Using a parameterized query to prevent SQL injection
var
mysql
=
require
(
'mysql2'
);
var
bcrypt
=
require
(
'bcrypt-nodejs'
);
// Prepare query parameters
var
username
=
req
.
body
.
username
;
var
passwordHash
=
bcrypt
.
hashSync
(
req
.
body
.
password
,
bcrypt
.
genSaltSync
());
// Make connection to the MySQL database
var
connection
=
mysql
.
createConnection
({
host
:
'localhost'
,
user
:
'db_user'
,
password
:
'secret'
,
database
:
'node_app_db'
});
connection
.
connect
();
// Execute prepared statement with parameterized user inputs
var
query
=
'SELECT * FROM accounts WHERE username=? AND password=?'
;
connection
.
query
(
query
,
[
username
,
passwordHash
],
function
(
err
,
rows
,
fields
)
{
console
.
log
(
"Results = "
+
JSON
.
stringify
(
rows
));
});
connection
.
end
();
If an attacker enters the username as admin' --
, the resultant query from the code in Example 1-7 would explicitly look for a username that matches the exact string admin' --
instead of admin
, thus foiling the SQL injection attack.
Using Stored Procedures Instead of Parameterized Queries
If SQL query construction takes place inside a stored procedure, generally stored procedures are safe and have the same effect as parameterized queries. However, you need to ensure that stored procedures do not use unsafe dynamic SQL generation by integrating user inputs.
Apply input validations based on whitelist
Validating user input data serves as an additional layer of protection against SQL injection. While writing the validation logic, compare the input against a whitelist of allowed options.
Besides SQL injection, input validation is also crucial in preventing other attacks, such as cross-site scripting (XSS), HTTP parameter pollution, denial-of-service, and other types of injection attacks.
Warning
Although input validation is a highly recommended practice and can detect erroneous input before passing it to the SQL query, it is not an alternative to using parameterized queries. Validated data is not necessarily safe to insert into dynamic SQL queries constructed by using string concatenation.
Use database accounts with least privilege
If an attacker becomes successful at SQL injection, to minimize the potential damage, use database accounts with the minimum required privileges. Here are some specific recommendations:
-
Never use a database account in Node application code that has admin-level rights.
-
Consider creating separate users with read-only and read-write access, and choose the user account with the minimum required privileges that meet the requirements.
-
When using stored procedures, restrict user account rights to allow executing only required, specific stored procedures.
-
Use SQL views to limit user account access to specific columns of a table or joins of tables.
-
If multiple applications share a common database, use different database accounts for each application.
-
Beyond database privileges, minimize the privileges of the operating system account that the database uses.
NoSQL Injection Attack Mechanics
Even though NoSQL databases do not use SQL syntax, it is possible to construct unsafe queries with user-supplied inputs. Hence, NoSQL databases are not inherently immune to injection attacks.
To illustrate NoSQL injection attacks, let’s consider a MongoDB find query, as shown in Example 1-10. I chose MongoDB for this example just because it is one of the popular NoSQL databases.
Example 1-10. A find query using MongoDB
db
.
accounts
.
find
({
username
:
post_username
,
password
:
post_password
});
In Example 1-10, post_username
and post_password
are user-supplied inputs.
In MongoDB, the $gt
comparison operator selects documents where the value of the field is greater than the specified value. Now, let’s consider a malicious input, as shown in a JSON object in Example 1-11.
Example 1-11. A malicious input object added to a find query
{
"post_username"
:
"admin"
,
"post_password"
:
{
$gt
:
""
}
}
With this input, the find query in Example 1-10 would compare if the password in the database is greater than an empty string, which would return true
, resulting in retrieving the admin user’s account data.
You can achieve the same results by using another comparison operator, such as $ne
, in the input.
Another mechanism to manifest NoSQL injection is exploiting the $where
operator, which takes a JavaScript function and processes user inputs within the function. Example 1-12 presents code that is vulnerable to just such a NoSQL injection:
Example 1-12. NoSQL Injection with the $where operator
db
.
myCollection
.
find
({
active
:
true
,
$where
:
function
()
{
return
obj
.
age
<
post_user_input
;
}
});
In example Example 1-12, the user input post_user_input
is used in an unsafe manner. If an attacker passes a valid JavaScript statement 0; while(1);
as a value for post_user_input
, the find query would run an infinite loop, making the database unresponsive.
The $where
operator-based NoSQL injection provides an attacker with the ability to craft malicious inputs using JavaScript language, which offers greater flexibility and options to an attacker when compared to a plain old SQL injection attack, in which strict SQL interpreter rules confine the ways to construct malicious inputs.
Preventing NoSQL Injection
Here are ways to prevent NoSQL injection:
-
There is no equivalent mechanism to parameterized SQL queries for NoSQL databases. To mitigate NoSQL injection, your best option is to validate and escape all user-supplied inputs before using it.
-
Avoid using options such as
$where
with JavaScript functions that directly process user-supplied inputs. -
Similar to SQL databases, using database accounts with the least privileges can limit the potential damage if an attacker becomes successful at NoSQL injection.
Conclusion
Although injection flaws are very prevalent, as we reviewed in this chapter, these security flaws are easy to avoid by applying safe coding practices. Using APIs that avoid passing untrusted data directly to an interpreter, validating user inputs, and applying the principle of least privileges are the key mechanisms to prevent it.
Additional Resources
Here are some more resources related to injection attacks:
- “Testing for SQL Injection”
-
Injection flaws are difficult to discover via testing. This article goes over techniques and tools to test for them.
- “Stored Procedure Attacks”
-
This article illustrates SQL injection attacks against stored procedures that are often assumed safe against SQL injection by default.
- Server-Side JavaScript Injection
-
This whitepaper explains server-side JavaScript injection when using
eval
in JavaScript code to parse JSON requests.
Get Securing Node Applications now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.