Understanding Injection Flaws: SQL, NoSQL, and Beyond

Understanding Injection Flaws: SQL, NoSQL, and Beyond

pr0h0
cybersecuritysql-injectionnosqlapplication-security
AI Usage (89%)

Why Injection Still Shows Up in Modern Apps

Injection bugs are old, but they keep turning up because modern apps still mix user input with executable context. That context might be SQL, a Mongo-style filter, a shell command, a template expression, or an LDAP query. The syntax changes. The failure mode stays the same.

I usually start with one question: where does data stop being data and start changing program structure? If the answer is “inside a string,” I get cautious fast.

This matters because injection is rarely just a parsing bug. In real apps, it often becomes an authorization bug, a data exposure bug, or a remote action bug. The query is only the first step.

SQL Injection: The Classic Case That Still Bites

SQL injection still shows up because developers build queries by hand under pressure. The mistake is often small and easy to miss in review.

Where parameterization stops and string building starts

This is the safe pattern:

const userId = req.params.id;
const row = await db.query("SELECT * FROM users WHERE id = ?", [userId]);

The unsafe pattern is usually one edit away:

const sql = `SELECT * FROM users WHERE id = ${req.params.id}`;
const row = await db.query(sql);

At that point, the database driver is no longer separating code from data. You are asking the parser to sort it out for you. That is where payloads become meaningful.

How authorization failures turn a query bug into real impact

The query bug is not always the whole story. A bad WHERE clause matters most when the backend does not verify ownership.

If a route accepts accountId and uses it directly in a query, a compromised filter can expose another tenant's rows. If the route also trusts the client to choose a status field, the same bug can turn into a write primitive.

Impact usually looks like this:

  • reading records from another user
  • changing data outside the caller's account
  • bypassing business rules that were only enforced in the UI

The backend has to enforce access control after parsing, not before.

NoSQL Injection: Same Shape, Different Syntax

NoSQL injection is easy to miss because it does not always look like classic string concatenation. In JavaScript code, the problem often shows up as object manipulation.

Operator smuggling and unsafe object merging

A common pattern is taking request JSON and merging it into a filter object:

const filter = { email: req.body.email };
Object.assign(filter, req.body.extra);

If untrusted input can influence query operators, the attacker may smuggle in fields like $ne, $gt, or $or. The exact shape depends on the driver and library, but the pattern is the same: user input changes query meaning.

This gets worse when developers treat request bodies as trusted objects. Unsafe merging can turn a simple lookup into “match anything.”

Common Node.js mistakes with query builders

I see three mistakes repeatedly:

  1. passing req.body directly into a query function
  2. whitelisting field names but not operator values
  3. assuming a library will reject dangerous keys by default

A query builder is not a sanitizer. It helps if you feed it a safe shape. It does not save you from logic that already accepted attacker-controlled operators.

Injection Outside the Database

Once you start looking for executable context, you see injection everywhere.

Command injection in server-side helpers

Server-side helpers often build shell commands for image processing, backups, or file inspection. If you interpolate user input into a command string, you are one shell parser away from trouble.

Use APIs that pass arguments as arrays, not strings. Better yet, avoid the shell entirely when you can.

Template injection and unsafe expression evaluation

Template engines are another common surface. If the app evaluates expressions from user content, the payload does not need to be a SQL keyword. It only needs to become code.

The risky patterns are:

  • eval() on user-controlled text
  • dynamic expression evaluators
  • rendering untrusted templates with server privileges

The defense is simple in principle and strict in practice: never hand an interpreter raw user input unless the feature is explicitly designed for it.

LDAP, XPath, and other less-discussed surfaces

LDAP and XPath injection show up in enterprise apps that nobody wants to audit until something breaks. They often hide in authentication, directory search, and document lookup code.

The bug shape is familiar:

  • query fragments are built with string concatenation
  • special characters are not escaped for the target grammar
  • the code assumes “internal app” means “safe app”

It does not.

A Small JavaScript Test Case You Can Reproduce

Building a toy route and watching the failure mode

Here is a minimal Express example that shows the pattern without doing anything destructive:

app.get("/search", async (req, res) => {
  const term = req.query.q;
  const sql = `SELECT id, title FROM notes WHERE title LIKE '%${term}%'`;
  const rows = await db.query(sql);
  res.json(rows);
});

Now compare it with a parameterized version:

app.get("/search", async (req, res) => {
  const term = req.query.q;
  const sql = "SELECT id, title FROM notes WHERE title LIKE ?";
  const rows = await db.query(sql, [`%${term}%`]);
  res.json(rows);
});

The second version still needs validation, but it no longer lets input rewrite the query structure.

Logging inputs, queries, and side effects

When I test a route like this, I log three things:

Thing to logWhy it matters
raw inputshows what the attacker controlled
final query shapeshows whether data became syntax
side effectshows whether the bug changed anything real

A good test report does not stop at “I changed the query.” It shows whether the app leaked rows, skipped authorization, or performed an action the user should not have been able to trigger.

How to Defend Without Guesswork

Use parameterized APIs and typed query builders

If a database driver supports parameters, use them. If a query builder supports typed placeholders, use those. Do not hand-roll escaping unless you have no alternative.

Validate shape, not just characters

Character filters are weak because valid payloads do not always look suspicious. Instead, validate:

  • expected type
  • allowed fields
  • allowed operators
  • allowed length and count

That approach works better for both SQL and NoSQL because it constrains structure, not just syntax.

Separate user input from executable context

This is the real rule behind all of it. User input can select data, but it should not define code, operators, or command structure.

If a feature needs dynamic behavior, translate user input into a safe internal representation first.

What Good Reporting Looks Like

A useful report should answer four questions:

  1. What input did the attacker control?
  2. Where did that input cross into executable context?
  3. What was the observable impact?
  4. What fix prevents the same mistake in the future?

If the report only says “injection exists,” it is incomplete. The backend owner needs the exact boundary that failed.

Conclusion

Injection flaws survive because they are not really about databases. They are about trust boundaries. SQL is just the most familiar place to notice them.

If you audit one thing, audit the places where raw input becomes a query, a command, or an expression. That is where the bug starts. The damage depends on what the backend does after that.

Share this post

More posts

Comments