Parameterizing MySQL Queries in Node
There are very few absolutes in software development. One of these would be “NEVER trust user input”. You should never run a database query involving user input without validating, sanitizing, and parameterizing the user-provided values. If you neglect to handle user input correctly, specifically parameterizing the data, your application will be vulnerable to attack from nefarious people. In this post, we will discuss how to parameterize user input in a Node.js application and how to use the SQL Template Tag module to make the process easier.
The Problem
When user data is not parameterized before being used in a database query, your database is vulnerable to attack from SQL injection. Using SQL injection, a hacker can gain access to your database server and cause irreparable harm. In a nutshell, SQL injection can occur when characters are passed as part of the user input that trick the database into running other commands.
When we parameterize a database query, we use placeholders in a query instead of constant values. We then pass the values that will be used with these placeholders. By using parameters, we are telling MySQL the values should be treated as specific data types and not part of a command that should be executed. Parameterized queries can mitigate the risk of SQL injection.
Getting the Code
This post will show how you can use parameterized queries in a Node application using the mysql2
Node module. We will also show how to use the SQL Template Tag module to make managing parameters easier.
You can clone the code from this GitHub Repo to follow along.
The command to clone the repo from the command line over SSH is:
git clone git@github.com:boyzoid/sql-template-tag-demo.git
Next, Change the directory for this demo.
cd sql-template-tag-demo
With the repo cloned, we need to install all the Node dependencies by running the following:
npm install
Run the SQL queries in /data/users.sql
on a MySQL instance.
Copy the .env.template
file to .env
and then update the values to match the connection information to the MYSQL instance you used above. The environment variables we use are:
PORT
- The port on which the Node application will run.DB_HOST
- The IP or domain address of the MySQL server.DB_PORT
- The port used to connect to the MySQL server.DB_USER
- The MySQL user account under which we will run the queries.DB_PASSWORD
- The password for the MySQL user account above.DB_SCHEMA
- The database schema we will use in the examples below.
Lastly, we can start our application by running the command:
node src
We can test that the application is working as expected by making a GET
HTTP request to http://localhost:{PORT}
where {PORT}
is the value of the PORT
environment variable. You should see the following response:
{
"message": "Main endpoint"
}
Code Overview
This repo has four code examples of how to handle user input. The application uses Express to define several API endpoints executing our code.
Let’s do a high-altitude overview of the code in /src/index.js
.
Import Modules
import express from 'express'
import sql from 'sql-template-tag'
import mysql from 'mysql2'
import * as dotenv from 'dotenv'
The code above will import the necessary modules into our application.
Read Environment Variables
//Init environment variables
dotenv.config()
This line of code will read the environment variables that define the database connection information and the port on which the Express application will be available.
Create MySQL Connection
//Create a connection to MySQL
const connection = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
database: process.env.DB_SCHEMA,
namedPlaceholders: true
});
In this code block, we are creating a connection to the MySQL server using the environment variables specified above.
Set up Express
// Define Express app
const app = express()
app.use(express.json())
This code will initialize the Express app.
Start Express
app.listen(process.env.PORT, () => {
console.log('listening on port ' + process.env.PORT)
});
This event handler starts the application and tells Express on which port to listen for requests.
Define the Routes
//Routes
// Default route
app.get('/', (req, res) => {
let msg = {message: 'Main endpoint'}
res.send(msg)
});
app.get('/unsafe/:id', (req, res) => {
// THIS IS VERY BAD - DON'T EVER WRITE A QUERY LIKE THIS
const query = 'SELECT * FROM user WHERE id = ' + req.params.id
connection.query(query, (err, results) => {
res.send({
_sql: query,
results: results
})
})
})
app.get('/safe/:id', (req, res) => {
// THIS IS THE WAY - the query is parameterized
const query = 'SELECT * FROM user WHERE id = ?'
const values = [req.params.id]
connection.query(query, values, (err, results) => {
res.send({
_sql: query,
_values: values,
results: results
})
})
})
app.get('/named/:id', (req, res) => {
// THIS IS THE WAY - the query is parameterized
const query = 'SELECT * FROM user WHERE id = :id'
const values = {id:req.params.id}
connection.query(query, values, (err, results) => {
res.send({
_sql: query,
_values: values,
results: results
})
})
})
app.get('/easier/:id', (req, res) => {
// This is easier. By using sql-template-tag and template literals
// the query and params are generated in one statement.
const query = sql`SELECT * FROM user WHERE id = ${req.params.id}`
connection.query(query, (err, results) => {
res.send({
_sql: query.sql,
_values: query.values,
results: results,
})
})
})
The code defines five routes for our Express app. The first is mapped to /
and returns a simple message. This route tests that the Express app is working as expected. I will go over the other four routes in depth below.
Code Deep Dive
Let’s look at the code we will use for our examples.
The Bad
The first route we will look at is the unsafe
route.
app.get('/unsafe/:id', (req, res) => {
// THIS IS VERY BAD - DON'T EVER WRITE A QUERY LIKE THIS
const query = 'SELECT * FROM user WHERE id = ' + req.params.id
connection.query(query, (err, results) => {
res.send({
_sql: query,
results: results
})
})
})
The route definition is for a GET
request to /unsafe
where a parameter named id
is required. So our request URL would look like http://localhost:3000/unsafe/63
.
With this code, we create a variable named query
that contains the query we wish to run and appends the id
parameter to the end of the query. Because we append the user input to the end of the query, it is NOT parameterized and is susceptible to SQL injection.
Next, we call the query()
method on the connection
object and pass the query. In the callback of connection.query()
, we set values in the res
(response) object. These values will be returned as part of the response.
When we make a request to http://localhost:3000/unsafe/63
, we will see the following response.
{
"_sql": "SELECT * FROM user WHERE id = 63",
"results": [
{
"id": 63,
"first_name": "Ringo",
"last_name": "Paolotto",
"email": "rpaolotto1q@arstechnica.com"
}
]
}
We can see that the value of _sql
is just a string with the id
parameter added.
The Good
To help mitigate attacks from SQL injection, we should rewrite the query above to use parameters. The following endpoint, named safe,
shows how to parameterize this query.
app.get('/safe/:id', (req, res) => {
// THIS IS THE WAY - the query is parameterized
const query = 'SELECT * FROM user WHERE id = ?'
const values = [req.params.id]
connection.query(query, values, (err, results) => {
res.send({
_sql: query,
_values: values,
results: results
})
})
})
The first difference is that instead of using req.params.id
in the query string, we use a question mark, a placeholder that tells MySQL we will be using a parameter here.
Next, you will note that we create a variable named values,
which is an array with a single element that contains the value of req.params.id
. We then pass this variable to connection.query()
, and the values defined will be used to determine the parameter values.
When we use parameters with this syntax, the length of the values
array must be, at least, the same number of placeholders. The first placeholder will use the first parameter, and so on.
When we make a request to http://localhost:3000/safe/63
, we will see the following response.
{
"_sql": "SELECT * FROM user WHERE id = ?",
"_values": [
"63"
],
"results": [
{
"id": 63,
"first_name": "Ringo",
"last_name": "Paolotto",
"email": "rpaolotto1q@arstechnica.com"
}
]
}
The Named
If we had a lot of placeholders, using the above syntax can lead to difficulty in determining which values are for which placeholder. Fortunately, the mysql2
module supports named parameters. Named parameters are turned off by default. We turned them on when we created the connection
object by passing namedParamters: true
as part of the connection config.
When using named parameters, the code for the query would look like this:
app.get('/named/:id', (req, res) => {
// THIS IS THE WAY - the query is parameterized
const query = 'SELECT * FROM user WHERE id = :id'
const values = {id:req.params.id}
connection.query(query, values, (err, results) => {
res.send({
_sql: query,
_values: values,
results: results
})
})
})
Here, we use the placeholder :id
installed of ?
in the query and use an object with matching keys for the values instead of an array. I prefer to use named parameters for a few reasons:
- It is easier to read.
- Knowing which values are being used for which placeholders is easier.
- You can reuse placeholders without needing to add another value.
- You can add placeholders and values more easily.
When we make a request to http://localhost:3000/named/63
, we will see the following response.
{
"_sql": "SELECT * FROM user WHERE id = :id",
"_values": {
"id": "63"
},
"results": [
{
"id": 63,
"first_name": "Ringo",
"last_name": "Paolotto",
"email": "rpaolotto1q@arstechnica.com"
}
]
}
An Easier Way
For queries with very few parameters, either of the examples we just showed should be easy to manage. But, if you have a large query that uses a lot of different parameters, it can still be unwieldy to manage the placeholders and the values. With the help of SQl Template Tag, we can use template literals to define the placeholders and the values in a single statement. Let’s look at the code for the easier
endpoint.
app.get('/easier/:id', (req, res) => {
// This is easier. By using sql-template-tag and template literals
// the query and params are generated in one statement.
const query = sql`SELECT * FROM user WHERE id = ${req.params.id}`
connection.query(query, (err, results) => {
res.send({
_sql: query.sql,
_values: query.values,
results: results,
})
})
})
You can see that we create a variable named query
and use the syntax sql`SELECT * FROM user WHERE id = ${req.params.id}`
which contains a template literal for the variable req.params.id
. The SQL Template Tag module then takes that variable and creates placeholders and a value array to use in the parameterized query. We can see these in the query.sql
and query.values
properties that are part of the response.
When we make a request to http://localhost:3000/easier/63
, we will see the following response.
{
"_sql": "SELECT * FROM user WHERE id = ?",
"_values": [
"63"
],
"results": [
{
"id": 63,
"first_name": "Ringo",
"last_name": "Paolotto",
"email": "rpaolotto1q@arstechnica.com"
}
]
}
Note that the query string contains a single placeholder, and the values array contains the value we pass as the id
.
Wrap-up
When writing database queries that include user input, we must ensure we parameterize the input to help mitigate an SQL injection attack. When we parameterize a query in a Node application, we can use unnamed placeholders, where we use ?
as the placeholder and pass an array of values to be used. If there are multiple placeholders or the same value may be used numerous times, we use named placeholders and pass an object with keys that match the parameter names. The SQL Template Tag module helps with this process by allowing us to create a parameterized query string and the value array in a single statement using template literals.
Photo by Caspar Camille Rubin on Unsplash