Frontend Engineering/
Lesson

You asked AI to build a user search endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.. It generated a clean Express route, wired up the database query, and the feature worked perfectly in testing. What you didn't notice: the AI used a template literal to build the SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. query. An attacker who types '; DROP TABLE users; -- into the search box can delete your entire user table. AI generates SQL injectionWhat is sql injection?An attack where user input is inserted directly into a database query, letting the attacker read, modify, or delete data. Parameterized queries prevent it. vulnerabilities more often than any other backend security flaw, because string interpolationWhat is interpolation?Inserting dynamic values like usernames or counts into a text template, replacing placeholders with actual data at display time. is the most "natural" way to build a query, and AI optimizes for natural-looking code.

How the attack works

When you build a SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. query by gluing user input into a string, you're trusting that the input contains only innocent data. An attacker exploits that trust by submitting input that contains SQL syntax instead:

// What AI typically generates
const query = `SELECT * FROM users WHERE username = '${username}'`;
db.query(query);

For a normal user, this produces a valid query:

SELECT * FROM users WHERE username = 'alice'

But an attacker submits alice' OR '1'='1 as the username. Your template literal produces:

SELECT * FROM users WHERE username = 'alice' OR '1'='1'

The OR '1'='1' condition is always true, so the query returns every row in the users table.

02

What attackers can do

The damage goes far beyond reading a few extra records.

AttackPayload exampleResult
Bypass authenticationadmin'-- as usernameLogs in as admin without password
Dump entire table' UNION SELECT password FROM users --Extracts all passwords through the UI
Delete data'; DROP TABLE users; --Destroys your user table
Read other tables' UNION SELECT credit_card FROM payments --Steals data from unrelated tables
Write files (some DBs)' INTO OUTFILE '/var/www/shell.php' --Creates a backdoor on the server
AI pitfall
When you ask AI to "create a search endpoint" or "add a login route," it frequently uses template literals or string concatenation to build SQL queries. This is the single most dangerous pattern in AI-generated backend code. Always check that AI-generated database queries use parameterized placeholders (CALLOUT, ?) instead of ${variable} inside the SQL string.
03

The fix: parameterized queries

Parameterized queries separate SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. code from data. You write the query template with placeholders, then pass the values separately. The database driver handles the separation, user input can never be interpreted as SQL syntax.

// PostgreSQL - CODE_BLOCK, $2, etc. are placeholders
const query = 'SELECT * FROM users WHERE username = CODE_BLOCK';
const result = await db.query(query, [username]);

// MySQL - ? is the placeholder
connection.query(
  'SELECT * FROM users WHERE username = ?',
  [username],
  (err, results) => { /* ... */ }
);

When the attacker submits alice' OR '1'='1, the database treats the entire string as the literal value to compare, not as SQL to evaluate. The query looks for a user whose username is literally alice' OR '1'='1, finds none, and returns zero rows.

Parameterized queries are sometimes called prepared statements. They are the same concept: the query structure is fixed, and user values are bound separately. Use this pattern everywhere you touch a database.
04

ORMs: protection by default

Object-Relational Mappers (ORMs) like Prisma, Sequelize, and TypeORM use parameterized queries for all standard operations:

// Prisma - parameterized internally
const user = await prisma.user.findUnique({
  where: { username: username }
});

// Sequelize - parameterized internally
const user = await User.findOne({
  where: { username: username }
});

The risk with ORMs comes when you drop into raw SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. for complex queries. Most ORMs provide a raw query method, use parameterization there too:

// Prisma raw query - template tag syntax IS parameterized
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE username = ${username}
`;

// But $queryRawUnsafe with string concatenation is NOT safe
// AI sometimes generates this when the template tag doesn't work
const users = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE username = '${username}'`  // VULNERABLE
);
AI pitfall
When AI generates Prisma code and the tagged template syntax causes a type error, it often "fixes" the issue by switching to $queryRawUnsafe with string interpolation. The function name literally says "unsafe," but AI uses it to avoid TypeScript complaints. If you see $queryRawUnsafe in AI-generated code, treat it as a red flag and check for interpolated user input.
05

The column name problem

Parameterization works for values (strings, numbers, booleans). It does not work for structural SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. elements like column names, table names, or sort directions. If your sort column comes from user input, a whitelist is the correct solution:

const allowedColumns = ['name', 'email', 'created_at', 'price'];

if (!allowedColumns.includes(req.query.sortBy)) {
  return res.status(400).json({ error: 'Invalid sort column' });
}

// Now safe - you've validated against a known list
const query = `SELECT * FROM products ORDER BY ${req.query.sortBy}`;

Never build column or table names from user input without a whitelist, even if you think the input is validated elsewhere.

06

Defense in depth

Parameterized queries are the main fix, but multiple layers make your application more resilient.

Least privilege database accounts

Your application's database user should have only the permissions it needs. A typical web app needs SELECT, INSERT, UPDATE, and DELETE on specific tables. It does not need DROP, CREATE, ALTER, or GRANT.

If an attacker does find an injection point, they're limited to what your app's database user can do.

Never expose raw errors

Database error messages reveal schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required. information, table names, column names, sometimes query structure. Catch database errors and return a generic message to the client:

try {
  const result = await db.query(query, params);
  res.json(result.rows);
} catch (error) {
  console.error('Database error:', error);  // log details server-side
  res.status(500).json({ error: 'An internal error occurred' });  // generic to client
}
07

Vulnerable vs safe patterns

PatternSafe?Why
` SELECT FROM users WHERE id = ${userId} | No | User input in SQL string | | 'SELECT FROM users WHERE id = TABLE' with [userId] | Yes | Parameterized | | User.findOne({ where: { id } }) | Yes | ORM uses parameterization | | SELECT FROM ${tableName} | Depends | Only safe with whitelist | | prisma.$queryRawSELECT WHERE id = ${id} | Yes | Template tag is parameterized | | prisma.$queryRawUnsafe(...${id}) `NoString concatenation bypasses safety
Testing your own application is legitimate and important. Security researchers commonly test forms and URL parameters with payloads like ' OR '1'='1 or admin'-- against their own systems. Never test against systems you don't own.
javascript
// SQL Injection: Vulnerable vs Safe

// VULNERABLE, what AI often generates
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const query = `
    SELECT * FROM users
    WHERE username = '${username}'
    AND password = '${password}'
  `;
  const user = await db.query(query);
});

// SAFE, parameterized query (PostgreSQL)
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const query = `
    SELECT * FROM users
    WHERE username = $1
    AND password = $2
  `;
  const result = await db.query(query, [username, password]);
  const user = result.rows[0];
});

// SAFE, Using ORM (Prisma)
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await prisma.user.findUnique({
    where: { username }
  });
  if (user && await bcrypt.compare(password, user.passwordHash)) {
    // Login success
  }
});

// SAFE, Whitelist for dynamic columns
app.get('/users', async (req, res) => {
  const { sortBy } = req.query;
  const allowedColumns = ['name', 'email', 'created_at'];
  if (!allowedColumns.includes(sortBy)) {
    return res.status(400).json({ error: 'Invalid sort column' });
  }
  const query = `SELECT * FROM users ORDER BY ${sortBy}`;
  const result = await db.query(query);
});