We've reached one of the oldest and most persistent families of vulnerabilities. A03 – Injection occurs when user-controlled data mixes with an instruction that an interpreter is about to execute —SQL, operating-system commands, LDAP, NoSQL queries— and the interpreter ends up executing part of that data as if it were instructions. In the 2021 Top Ten this category grew: it absorbed the old Cross-Site Scripting (XSS), which is technically an injection in the browser. Here we cover server-side injection; XSS gets its own in-depth treatment in lesson 03-04, so we only mention it here as a close relative.
The root cause is always the same: failing to separate code from data. In BazarNube we'll see a SQL injection in the product search of the Node/PostgreSQL API, and a command injection in a utility of the legacy Java module. The impact ranges from reading the entire database to running commands on the server.
Legal and ethical notice: the
payloadsshown are illustrative and use fake data. Practice only on your own systems or with explicit authorization. Launching injections against other people's systems is a crime.
Contents
- What injection is and why it happens
- SQL injection (SQLi) in the BazarNube API
- Parameterized queries and ORMs
- Operating-system command injection
- LDAP and NoSQL injection
- Input validation and least privilege
- XSS: the browser's injection (deferred to 03-04)
- Common mistakes, exercises and solutions
- What injection is and why it happens
An interpreter receives a string and decides what is instruction and what is data. If we build that string by concatenating user input, the user can introduce metacharacters (', ;, --, $) that break the intended structure and change the meaning of the instruction.
flowchart LR
A[User input] --> B{Concatenated into the instruction?}
B -- Yes --> C[The interpreter runs data as code]
B -- No, passed as a parameter --> D[The data never changes the structure]
C --> E[Injection]
D --> F[Safe]
The underlying defense is to separate code from data: the developer defines the structure of the instruction and the engine treats user input always as a value, never as syntax.
- SQL injection in BazarNube
The API's product search was written by concatenating the search term:
// routes/search.js — VULNERABLE to SQLi
router.get('/api/products/search', async (req, res) => {
const q = req.query.q;
const sql = "SELECT id, name, price FROM products WHERE name LIKE '%" + q + "%'";
const result = await db.query(sql); // direct concatenation
res.json(result.rows);
});How it's exploited (illustrative)
With a normal search ?q=tshirt, the query is correct. But an attacker sends:
The resulting query becomes:
SELECT id, name, price FROM products WHERE name LIKE '%x%'
UNION SELECT id, email, pwd FROM users --%'The UNION adds the emails and password hashes from the users table to the results, and -- comments out the rest. The product search ends up leaking credentials. Variants of this technique allow reading any table, deleting data (; DROP TABLE ...) or extracting information blindly (blind SQLi) by measuring response times.
Fix: parameterized queries
// routes/search.js — SECURE with a parameterized query
router.get('/api/products/search', async (req, res) => {
const q = req.query.q ?? '';
const sql = 'SELECT id, name, price FROM products WHERE name LIKE $1';
const result = await db.query(sql, ['%' + q + '%']); // q is passed as a PARAMETER
res.json(result.rows);
});The difference is essential: $1 is a placeholder. The PostgreSQL driver sends the query structure and the values separately; the engine never interprets the content of q as SQL. Even if q contains ' UNION SELECT ..., it will be searched literally within the product name. Code and data stay separate.
- Parameterized queries and ORMs
| Approach | Example | Security |
|---|---|---|
| String concatenation | "... WHERE id=" + id |
Vulnerable |
| Parameterized query | query(sql, [id]) |
Safe |
| ORM / query builder | Product.findByPk(id) |
Safe (uses parameters under the hood) |
An ORM like Sequelize or Prisma generates parameterized queries automatically, which reduces the risk. Watch out: that protection is lost if you use the ORM's "escape hatch" for raw SQL and concatenate input:
// DANGER: even with an ORM, this is vulnerable again
sequelize.query("SELECT * FROM products WHERE name = '" + name + "'");
// Correct: use replacements/bind
sequelize.query('SELECT * FROM products WHERE name = ?', { replacements: [name] });
- Operating-system command injection
The legacy Java module has a utility that generates thumbnails by invoking an external tool. It was built by concatenating the file name:
// ThumbnailService.java — VULNERABLE to command injection
public void generate(String filename) throws IOException {
// filename comes from a user request
String cmd = "convert /uploads/" + filename + " -resize 100x100 /thumbs/" + filename;
Runtime.getRuntime().exec(cmd); // runs a string in the shell
}If filename is photo.png; rm -rf /data, the shell runs two commands. The attacker achieves command execution on the server. Fix: don't invoke a shell and pass the arguments separately, plus validate the name.
// ThumbnailService.java — SECURE
public void generate(String filename) throws IOException, InterruptedException {
if (!filename.matches("[A-Za-z0-9_.-]{1,64}")) { // strict allowlist
throw new IllegalArgumentException("Invalid file name");
}
ProcessBuilder pb = new ProcessBuilder(
"convert", "/uploads/" + filename, "-resize", "100x100", "/thumbs/" + filename);
// ProcessBuilder does NOT use a shell: each argument is passed as-is, without interpreting ; | &
pb.start().waitFor();
}ProcessBuilder with separate arguments prevents the shell from interpreting metacharacters, and the allowlist (matches) rejects any name that isn't alphanumeric. A double barrier.
- LDAP and NoSQL injection
The same principle (separate code from data) applies to other interpreters:
- LDAP: building filters by concatenating input allows injecting
*)(uid=*to bypass authentication. Solution: escape according to RFC 4515 or use APIs that parameterize the filter. - NoSQL (MongoDB): sending objects instead of strings allows operators. If login does
find({user, pass})withpasstaken straight from the JSON, an attacker sends{"pass": {"$ne": null}}and gets "any password other than null."
// NoSQL — VULNERABLE: accepts objects from the client
db.users.findOne({ user: req.body.user, pass: req.body.pass });
// SECURE: force primitive types before querying
const user = String(req.body.user), pass = String(req.body.pass);
db.users.findOne({ user, pass }); // (and also compare the hash, see A02)Forcing the types (String(...)) prevents the client from slipping in operators like $ne or $gt.
- Input validation and least privilege
Parameterization is the primary defense, but it's reinforced with defense in depth:
- Allowlist input validation: accept only what's expected (type, length, format). Don't rely on "denylists" of forbidden characters; there's always a way around them.
- Least privilege in the database: the account the BazarNube API uses doesn't need to be a superuser or be able to
DROP TABLE. If it only reads/writes certain tables, a SQLi has far less reach. - Escaping as a last resort: when you can't parameterize (e.g. a dynamic column name), validate against an allowlist of permitted values, never concatenate directly.
- Limit results and errors: don't return raw SQL errors to the client (they make exploitation easier; see A05).
| Layer | What it adds |
|---|---|
| Parameterized query / ORM | Eliminates injection at the source |
| Allowlist validation | Rejects malformed input before it reaches the engine |
| Least privilege in the DB | Reduces the impact if something fails |
| Generic errors | Give the attacker no hints |
- XSS: the browser's injection
Cross-Site Scripting is also injection, but the interpreter is the browser (it runs injected JavaScript) instead of the database. Because of its importance and its own techniques (contextual output encoding, CSP, React escaping), we cover it in its own lesson: 03-04 – Cross-Site Scripting (XSS) in Depth. Here we only note that it shares the same root: mixing user data with code that another engine is going to run.
Common Mistakes and Tips
- Concatenating input into queries. Even if it "looks like a number," always parameterize.
- Relying on character denylists. They're bypassable; use allowlists and parameterization.
- Escaping SQL by hand. It's fragile; leave escaping to the driver via parameters.
- Using the ORM and then falling back to a concatenated
raw query. It cancels all protection. - Passing input to a shell. Avoid
Runtime.exec(string)/child_process.exec; use APIs with separate arguments. - A DB account with full permissions. Apply least privilege: limit tables and operations.
- Tip: enable a SAST/DAST scanner in CI (we'll see it with ZAP in M6) to detect dangerous concatenations.
Exercises
Exercise 1. Rewrite this BazarNube endpoint securely:
router.get('/api/orders', async (req, res) => {
const status = req.query.status;
const rows = await db.query(
"SELECT * FROM orders WHERE user_id = " + req.session.userId +
" AND status = '" + status + "'");
res.json(rows.rows);
});Exercise 2. The Java module's login uses "... WHERE user='" + u + "' AND pass='" + p + "'". Show a payload that bypasses authentication and explain why parameterization prevents it.
Exercise 3. Why is ProcessBuilder("convert", filename, ...) safer than Runtime.exec("convert " + filename)? Is it still advisable to validate filename?
Solutions
Solution 1. Parameterize both values (including user_id, even though it comes from the session, for consistency):
router.get('/api/orders', async (req, res) => {
const rows = await db.query(
'SELECT * FROM orders WHERE user_id = $1 AND status = $2',
[req.session.userId, req.query.status]);
res.json(rows.rows);
});Solution 2. With u = admin' -- the query becomes ... WHERE user='admin' --' AND pass='...': the -- comments out the password check and the attacker gets in as admin. With parameters, admin' -- would be searched literally as a username (which doesn't exist), because the engine never interprets that string as SQL syntax.
Solution 3. ProcessBuilder with separate arguments does not launch a shell, so metacharacters like ;, | or & are passed as literal text to convert, not interpreted as command separators. Even so, it's advisable to validate filename with an allowlist: it prevents malicious paths (../../etc/passwd) and inputs that break the tool. Defense in depth.
Conclusion
Injection is defeated by separating code from data: parameterized queries or an ORM for SQL, APIs with separate arguments for OS commands, type coercion for NoSQL, and proper escaping for LDAP, all reinforced with allowlist validation and least privilege. It's a mechanical and highly effective defense: applied well, it almost entirely eliminates the category.
Backlog entry — A03: parameterized the product search and the order listing; replaced Runtime.exec with ProcessBuilder plus validation in ThumbnailService; type coercion in the NoSQL login; created a DB account with minimal permissions for the API.
We mentioned that XSS is the injection that runs in the browser. It's so relevant to BazarNube's React front end that we devote the entire next lesson to it: Cross-Site Scripting (XSS) in Depth.
OWASP Course: Guidelines and Standards for Web Application Security
Module 1: Introduction to OWASP
Module 2: Main OWASP Projects
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Other Key Projects: WSTG, Cheat Sheets and Dependency-Check
Module 3: OWASP Top Ten 2021 in Depth
- A01:2021 – Broken Access Control
- A02:2021 – Cryptographic Failures and Sensitive Data Exposure
- A03:2021 – Injection
- Cross-Site Scripting (XSS) in Depth
- A04:2021 – Insecure Design
- A05:2021 – Security Misconfiguration
- XML External Entities (XXE)
- A06:2021 – Vulnerable and Outdated Components
- A07:2021 – Identification and Authentication Failures
- A08:2021 – Software and Data Integrity Failures (Insecure Deserialization)
- A09:2021 – Security Logging and Monitoring Failures
- A10:2021 – Server-Side Request Forgery (SSRF)
Module 4: OWASP ASVS (Application Security Verification Standard)
Module 5: OWASP SAMM (Software Assurance Maturity Model)
Module 6: OWASP ZAP (Zed Attack Proxy)
- Introduction to ZAP
- Installation and Configuration
- Vulnerability Scanning
- Automating Security Testing
Module 7: Best Practices and Recommendations
- Secure Software Development Life Cycle (SDLC)
- Threat Modeling
- Integrating Security into DevOps (DevSecOps)
- Security Training and Awareness
- Additional Tools and Resources
Module 8: Practical Exercises and Case Studies
- Exercise 1: Identifying Vulnerabilities
- Exercise 2: Implementing Security Controls
- Case Study 1: Analyzing a Security Incident
- Case Study 2: Improving the Security of a Web Application
