Vulnerabilities

What Is SQL Injection?

A vulnerability where an attacker inserts malicious SQL code into a query through user input. If the application passes user input directly into SQL queries without sanitization, the attacker can read, modify, or delete data, and in some cases execute commands on the database server.

How It Works

SQL injection works by inserting SQL grammar into user-supplied input that reaches a database query, causing the database to interpret the input as commands rather than data.

Step 1: Identifying the injection point — The attacker probes inputs that are likely to reach the database: search fields, URL ID parameters (/users/123), filter dropdowns, and sort parameters. They submit a single apostrophe ' and observe the response — a SQL syntax error, a different result set, or a change in behavior confirms the parameter reaches a query without parameterization.

Step 2: Determining the query structure — To construct a UNION SELECT exploit, the attacker must know how many columns the original query selects. They submit 1 ORDER BY 1--, then 1 ORDER BY 2--, incrementing until a SQL error appears, indicating the column count has been exceeded. With five columns confirmed, they proceed.

Step 3: UNION-based extraction — The attacker constructs: 1 UNION SELECT username, password, email, null, null FROM users--. The database executes both the original query and the attacker's SELECT, returning all rows from the users table merged with the original result set. The application renders both, revealing every username, hashed password, and email.

Step 4: Blind injection (when no output is visible) — If the application does not display query results directly, the attacker uses boolean-based blind injection. 1 AND SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a' returns true (normal response) or false (empty/error response). By iterating through characters, the attacker extracts data one character at a time. Time-based blind uses AND SLEEP(5)-- to infer data from response timing when even response differences are invisible.

Step 5: Escalation — In MySQL with FILE privilege, LOAD_FILE('/etc/passwd') reads server files. INTO OUTFILE writes webshells to disk. In some configurations, this enables full server compromise from a single SQL injection vulnerability.

Types of SQL Injection

UNION-Based Injection

Attacker appends a UNION SELECT statement to extract data from arbitrary tables in the same database — results are returned directly in the application response.

GET /products?category=1 UNION SELECT username,password,email,null FROM users-- HTTP/1.1

Error-Based Injection

Database error messages are engineered to contain query output — effective when the application displays database errors (which `APP_DEBUG=true` does in Laravel).

GET /users?id=1 AND extractvalue(1,concat(0x7e,(SELECT password FROM users LIMIT 1))) HTTP/1.1

Boolean-Based Blind Injection

Attacker infers data one bit at a time by observing whether the page returns a normal response (true condition) or an empty/error response (false condition).

# True: page loads normally
GET /users?id=1 AND 1=1--
# False: page returns no results
GET /users?id=1 AND 1=2--

Time-Based Blind Injection

Attacker extracts data by measuring server response time — a `SLEEP(5)` that causes a 5-second delay confirms the condition is true.

GET /users?id=1 AND IF(1=1,SLEEP(5),0)-- HTTP/1.1
# 5-second delay confirms injection; adjust condition to extract data

In Laravel Applications

Laravel's Eloquent ORM uses parameterized queries by default, which prevents SQL injection. Vulnerabilities are introduced when using DB::raw(), DB::statement(), or whereRaw() with unescaped user input. Always use parameter binding: DB::select("SELECT * FROM users WHERE email = ?", [$email]).

Code Examples

Raw Query Interpolation vs. Parameterized Binding

Vulnerable

// VULNERABLE: User input concatenated into SQL string
// Attacker sends: id=1 UNION SELECT email,password,null FROM users--
public function show(Request $request): JsonResponse
{
    $results = DB::select(
        "SELECT * FROM products WHERE id = " . $request->input('id')
    );
    return response()->json($results);
}

// Also vulnerable with whereRaw:
Product::whereRaw("name = '" . $request->input('name') . "'")->get();

Secure

// SECURE: User input passed as bound parameter — treated as data, not SQL
public function show(Request $request): JsonResponse
{
    // Option 1: Parameterized DB::select with array binding
    $results = DB::select(
        "SELECT * FROM products WHERE id = ?",
        [$request->input('id')]
    );

    // Option 2: Eloquent (uses PDO prepared statements by default)
    $product = Product::findOrFail($request->input('id'));

    // Option 3: whereRaw with bound parameters (safe)
    $products = Product::whereRaw("name = ?", [$request->input('name')])->get();

    return response()->json($results);
}

Real-World Example

DB::select("SELECT * FROM users WHERE id = " . $request->id) is vulnerable. An attacker can send id=1 OR 1=1 to dump all users. Use DB::select("SELECT * FROM users WHERE id = ?", [$request->id]) instead.

Why It Matters

SQL injection remains one of the most critical vulnerabilities in web applications because the consequences are severe: complete database extraction, authentication bypass, and in some database configurations, operating system command execution. Despite being one of the oldest known vulnerabilities, it continues to appear in Laravel applications where developers bypass the ORM's protections.

Eloquent's Query Builder uses PDO prepared statements and parameterized queries by default, which is the correct defense against SQL injection. The vulnerability is introduced specifically in four "raw" methods: DB::raw(), whereRaw(), orderByRaw(), and selectRaw(). When user input is string-concatenated into these methods rather than passed as a bound parameter, SQL injection becomes possible.

The impact in Laravel is compounded by the fact that the database user configured in config/database.php often has broad permissions (SELECT, INSERT, UPDATE, DELETE on all tables) for development convenience. In production, principle of least privilege dictates separate database users for different application roles, but this is rarely implemented.

Laravel's $fillable and $guarded properties on Eloquent models prevent mass assignment vulnerabilities, which are a related but distinct class of injection. The $guarded = [] shortcut — used to disable mass assignment protection entirely — combined with Model::create($request->all()) can allow attackers to set arbitrary model attributes.

Common Misconceptions

Myth: We use Laravel's query builder, so SQL injection is impossible.

Reality: The query builder is safe when used correctly, but `whereRaw()`, `orderByRaw()`, `selectRaw()`, and `DB::raw()` all bypass parameterization if you string-interpolate user input directly into the expression.

Myth: Escaping user input with `addslashes()` or `htmlspecialchars()` prevents SQL injection.

Reality: Input escaping is an unreliable defense against SQL injection. Parameterized queries (which Eloquent uses by default) are the correct and only reliable defense. Escaping functions have known bypasses and do not cover all edge cases.

Myth: SQL injection only lets attackers read data, not modify it.

Reality: SQL injection can enable SELECT (read all data), INSERT/UPDATE/DELETE (modify data), and in some configurations, file system access via `LOAD_FILE` or `INTO OUTFILE` on MySQL.

How to Detect This

Search your codebase for raw query usage with `grep -rn "whereRaw\|orderByRaw\|selectRaw\|DB::raw\|DB::statement\|DB::select" app/` and review each occurrence to verify user input is passed as bound parameters (second array argument) rather than interpolated into the string. OWASP ZAP's active scan automatically tests for SQL injection by submitting malicious payloads in form fields and URL parameters. StackShield monitors for unusual error patterns in your application that may indicate SQL injection probing. In your database server logs, look for queries containing SQL keywords like `UNION SELECT`, `OR 1=1`, or `DROP TABLE` in the query text.

How to Prevent This in Laravel

  1. 1

    Run `grep -rn "whereRaw\|orderByRaw\|selectRaw\|DB::raw\|DB::select\|DB::statement" app/` and review every result — verify user input is in the second parameter array, never interpolated into the SQL string.

  2. 2

    Use Eloquent ORM for all standard CRUD operations — `User::find()`, `User::where()->get()`, `User::create()` all use PDO prepared statements automatically.

  3. 3

    When you must use raw methods, always pass user input as bound parameters: `whereRaw("column = ?", [$userInput])` not `whereRaw("column = '$userInput'")`.

  4. 4

    Configure the database user in `config/database.php` with minimum required permissions — `SELECT`, `INSERT`, `UPDATE`, `DELETE` on your application's tables only, never `FILE` or `SUPER` privileges.

  5. 5

    Enable query logging in development with `DB::enableQueryLog()` and periodically review `DB::getQueryLog()` to verify user input never appears embedded in the SQL string — it should always appear as a separate binding.

Frequently Asked Questions

Does using Eloquent ORM prevent all SQL injection in Laravel?

Eloquent's standard query methods (where, find, first, get, create, update, delete) use PDO prepared statements and are safe against SQL injection. The vulnerability is introduced specifically through the raw methods: `DB::raw()`, `whereRaw()`, `orderByRaw()`, `selectRaw()`, `DB::select()`, and `DB::statement()`. When you use any of these with string-concatenated user input instead of bound parameter arrays, SQL injection is possible.

Is `whereRaw()` ever safe to use?

`whereRaw()` is safe when user input is passed as the second parameter (bound parameters array): `->whereRaw("status = ? AND priority > ?", [$status, $priority])`. It is vulnerable when user input is string-interpolated into the first parameter (the SQL expression): `->whereRaw("name = '$name'")`. The rule is simple: if you see a PHP variable inside the SQL string argument, it is vulnerable.

Can stored procedures in MySQL prevent SQL injection in Laravel?

Stored procedures help only if they also use parameterized queries internally — a stored procedure that concatenates user input into dynamic SQL is still vulnerable. In Laravel, calling a stored procedure via `DB::statement("CALL my_proc(?)", [$param])` is safe because the parameter is bound. The binding happens at the PHP/PDO level, before any SQL reaches the database engine, regardless of whether the target is a table query or a stored procedure.

How do I test my Laravel application for SQL injection?

Start with a code review: `grep -rn "whereRaw\|DB::raw\|DB::select" app/` and manually inspect each call. For dynamic testing, run OWASP ZAP's active scan against your staging environment — it automatically submits SQL injection payloads to every discovered form field and URL parameter. For comprehensive testing, use sqlmap against specific endpoints: `sqlmap -u "https://staging.yourapp.com/search?q=test" --dbs` which attempts dozens of injection techniques automatically.

Related Terms

Related Articles

Related Fix Guides

Monitor Your Laravel Application's Security

StackShield continuously checks your Laravel application from the outside, catching security issues before attackers find them.

Start Free Trial