DEV Community

khg5293
khg5293

Posted on

Why Parameterized Queries Matter for SQL Security

SQL injection is one of the classic examples of what can happen when an application mixes user input directly into a database query.

The underlying problem is simple.

The application expects data.

The database may interpret part of that data as SQL code.

That is why parameterized queries are such an important security control.

The unsafe approach

Imagine a simple project lookup page where a user submits a project name.

An unsafe query might be built like this:

const projectName = request.body.projectName;

const query =
  "SELECT * FROM projects WHERE name = '" +
  projectName +
  "'";

db.query(query);
Enter fullscreen mode Exit fullscreen mode

If the input is:

khg5293-json-formatter
Enter fullscreen mode Exit fullscreen mode

the final query becomes:

SELECT * FROM projects
WHERE name = 'khg5293-json-formatter';
Enter fullscreen mode Exit fullscreen mode

That looks fine.

The problem is that the application is directly combining input with SQL syntax.

Why this becomes dangerous

An attacker is not required to submit the value the application expects.

They may provide input containing characters that change the meaning of the query.

Once user input becomes part of the SQL statement itself, the database may interpret it as executable SQL instead of ordinary data.

That is the security boundary we want to preserve.

The application should control the SQL structure.

The user should only control the values being supplied to that structure.

Parameterized queries separate code from data

A safer version looks like this:

const projectName = request.body.projectName;

db.query(
  "SELECT * FROM projects WHERE name = ?",
  [projectName]
);
Enter fullscreen mode Exit fullscreen mode

Here, the SQL statement and the user supplied value are passed separately.

The database driver handles the parameter as data rather than inserting it directly into the SQL syntax.

This is much easier to reason about and significantly safer.

Another example

Imagine a page that retrieves a project using a numeric ID.

A parameterized query could look like this:

const projectId = Number(request.params.id);

if (
  !Number.isInteger(projectId) ||
  projectId <= 0
) {
  throw new Error("Invalid project ID");
}

db.query(
  "SELECT * FROM projects WHERE id = ?",
  [projectId]
);
Enter fullscreen mode Exit fullscreen mode

This combines two useful controls.

First, the server validates that projectId is actually a positive integer.

Second, the value is passed into the SQL query as a parameter.

Validation and parameterization solve different problems, but they work well together.

Input validation alone is not enough

It is tempting to think that filtering suspicious characters is enough to prevent SQL injection.

For example:

const input = request.body.projectName;

if (input.includes("'")) {
  throw new Error("Invalid input");
}
Enter fullscreen mode Exit fullscreen mode

This is not a good primary defense.

Attack techniques can vary, database syntax can vary, and maintaining a list of every dangerous pattern is difficult.

The safer approach is to design the query so that user input is never interpreted as SQL syntax in the first place.

That is exactly what parameterized queries are designed to do.

Avoid building queries manually

Consider this:

const owner = request.body.owner;
const status = request.body.status;

const query =
  "SELECT * FROM projects WHERE owner = '" +
  owner +
  "' AND status = '" +
  status +
  "'";

db.query(query);
Enter fullscreen mode Exit fullscreen mode

The more values that are concatenated into a query, the harder the code becomes to reason about safely.

A parameterized version is much cleaner:

db.query(
  "SELECT * FROM projects WHERE owner = ? AND status = ?",
  [owner, status]
);
Enter fullscreen mode Exit fullscreen mode

The SQL structure stays fixed.

Only the values change.

Parameterized queries also improve readability

Security is the main benefit, but parameterized queries usually make code easier to read as well.

For example:

const project = {
  owner: "khg5293",
  language: "TypeScript",
  status: "active"
};

db.query(
  "INSERT INTO projects (owner, language, status) VALUES (?, ?, ?)",
  [
    project.owner,
    project.language,
    project.status
  ]
);
Enter fullscreen mode Exit fullscreen mode

It is immediately clear which values are being inserted and where they belong.

The developer does not have to manually worry about quoting or concatenating each value.

Parameterization is not the entire security model

Parameterized queries are extremely important, but they are still only one layer.

Applications should also consider:

  • Input validation
  • Authentication
  • Authorization
  • Least privilege database accounts
  • Secure error handling
  • Logging and monitoring
  • Rate limiting where appropriate

For example, a perfectly parameterized query is still a problem if any user can access data they are not authorized to see.

Security controls solve different parts of the problem.

A useful mental model

I like to think of it this way:

SQL belongs to the application.

Data belongs to the user.

The application should never let user supplied data become part of the SQL language itself.

For example:

db.query(
  "SELECT * FROM projects WHERE owner = ?",
  ["khg5293"]
);
Enter fullscreen mode Exit fullscreen mode

This is fundamentally safer than constructing the SQL statement by combining strings.

Final thought

Parameterized queries are not complicated, but they solve an important problem.

They create a clear separation between SQL instructions and user supplied data.

Input validation is still valuable.

Authorization is still necessary.

Other defensive layers still matter.

But when an application interacts with a SQL database, parameterization should be the default way to handle values.

Top comments (0)