In Laravel's query builder, which parts of a query are bound as parameters and which are pasted in verbatim, and how do selectRaw, whereRaw and DB::raw change that?
answer
- values bound, identifiers not
- DB::raw returns an Expression
- Raw methods accept a bindings array
- ? placeholders in raw strings
- never user input as a column
basics
~20 sValues passed to where(), whereIn(), insert() and update() are bound as PDO parameters. Column and table names are only quoted, and DB::raw() or selectRaw/whereRaw/orderByRaw strings are pasted verbatim, so user input there must go through their ? bindings or an allowlist.
solid answer
~40 sLaravel's builder binds **values**: the third argument of `where()`, the list in `whereIn()`, and the arrays given to `insert()` and `update()` become `?` placeholders sent to PDO. **Identifiers** are different: column and table names are wrapped in the grammar's quote characters but never bound, and the docs warn never to let user input choose them, including the `orderBy` column. **Raw expressions** are pasted as-is: `DB::raw()` returns an `Expression` whose string goes straight into the SQL, and so do `selectRaw`, `whereRaw`, `havingRaw`, `orderByRaw` and `groupByRaw`. Those raw methods accept a bindings array, so `whereRaw('lower(email) = ?', [$email])` is safe while `whereRaw("lower(email) = '$email'")` is injectable. `orderBy()` rejects any direction other than `asc` or `desc`, but the column still needs an allowlist.
code
php · 15 lines<?php
use Illuminate\Support\Facades\DB;
$sort = match ($request->input('sort')) {
'price' => 'price',
'created' => 'created_at',
default => 'name',
};
$products = DB::table('products')
->where('name', 'like', '%'.$request->input('q').'%') // bound value
->selectRaw('price_cents * ? as gross_cents', [1.2]) // bound inside raw
->orderBy($sort) // allowlisted identifier
->get();go deeper
Recall that where() values are bound but DB::raw() and the Raw methods paste their strings into the SQL.
Explain the three categories, values, identifiers and raw expressions, and how the bindings array of selectRaw or whereRaw carries input safely.
Review a diff for interpolated raw strings and request-driven identifiers, and prove a fix by reading toSql() and the bindings.
Set a team rule that raw strings stay literal and identifiers come from allowlists, and decide how that is enforced in review or static analysis.
## Three kinds of input to a builder call Everything you hand Laravel's query builder falls into one of three categories, and each is treated differently when the SQL is compiled. 1. **Values** are data compared against or written to a column. They become `?` placeholders in the SQL text, and the actual values travel separately to PDO as **bindings**. The docs state there is no need to clean strings passed as bindings. 2. **Identifiers** are table and column names. The grammar **wraps** them in its quote character (backticks on MySQL, double quotes on PostgreSQL and SQLite) and escapes that character, but they are part of the SQL text, not bindings. PDO cannot bind an identifier. 3. **Raw expressions** are strings you explicitly ask Laravel to paste into the SQL unchanged. The docs warn that Laravel cannot guarantee that any query using raw expressions is protected against injection. ## Where each appears | Call | Bound (safe for input) | Pasted into SQL | |---|---|---| | `where('email', $email)` | `$email` | `email` identifier | | `whereIn('id', $ids)` | every element of `$ids` | `id` identifier | | `orderBy($column, $dir)` | nothing | `$column`; `$dir` must be `asc` or `desc` | | `selectRaw('price * ? as gross', [$rate])` | `$rate` | the expression string | | `whereRaw('lower(email) = ?', [$email])` | `$email` | the expression string | | `DB::raw("count($col)")` | nothing | the whole string | `DB::raw()` has **no bindings parameter**: it returns an `Illuminate\Database\Query\Expression` that simply wraps a string. The `...Raw` methods on the builder (`selectRaw`, `whereRaw`, `orWhereRaw`, `havingRaw`, `orderByRaw`, `groupByRaw`) all accept an array of bindings as their second argument, and that array is how you get user input into a raw fragment safely. ## A review of a real diff A pull request adds a product search to a coffee-shop admin: ```php DB::table('products') ->whereRaw("name like '%{$request->q}%'") ->orderBy($request->input('sort', 'name')) ->get(); ``` Both lines are defects: - **The `whereRaw` string interpolates input.** A search term containing a quote rewrites the SQL. The fix keeps the raw fragment constant and binds the value: `->where('name', 'like', '%'.$term.'%')`, or `->whereRaw('name like ?', ['%'.$term.'%'])` when a raw form is genuinely needed. - **The sort column comes from the request.** Identifier wrapping makes classic break-outs harder, but the docs are explicit that PDO cannot bind column names and user input should never choose them. Map the input through an allowlist, for example `match ($request->input('sort')) { 'price' => 'price', default => 'name' }`, and pass only the matched constant to `orderBy()`. ## The direction argument is validated `orderBy()` lower-cases a string direction and throws `InvalidArgumentException` unless it is `asc` or `desc`. That protects the direction slot, which is why a free-text direction is not the risk; the column slot has no such check, so the column is where an allowlist is needed. ## Habits that keep raw SQL safe - Keep every raw string a **literal in your code**; anything variable goes in the bindings array. - Prefer a builder method over a raw one when it exists: `whereColumn`, `whereIn`, `whereBetween`, `whereAny` and `joinSub` cover most cases people reach for raw SQL to solve. - Remember that a raw fragment is also **dialect-specific**: `strftime` on SQLite, `date_format` on MySQL and `to_char` on PostgreSQL are not interchangeable, and the skeleton's default connection is SQLite. - Check the compiled SQL with `toSql()`, which shows the `?` placeholders; if user text appears in the SQL itself, the value was pasted rather than bound. - `DB::raw()` returns an object with no string conversion, so concatenating it with `.` fails. Build the string first, from constants, and wrap it once.
- How can you confirm from code that a Laravel query binds a value instead of pasting it?Call `toSql()` and inspect the text: bound values appear as `?` and show up separately in `getBindings()`. If the user's text is visible inside the `toSql()` string, it was concatenated into a raw fragment. `toRawSql()` substitutes the bindings back for reading, so it is the wrong tool for this particular check.
- Is whereIn('id', $request->input('ids')) safe?For injection, yes: each element becomes its own `?` binding. It still needs validation that the input is a bounded list of integers, because a huge array produces a huge placeholder list. An empty array compiles to the always-false `0 = 1`, so it returns no rows rather than failing.
saying these in an interview costs you the question
- Believes DB::raw() escapes the string it wraps
- Thinks column names passed to orderBy() are bound parameters
- Assumes whereRaw is safe because Laravel uses PDO
- Passes request input as the orderBy column without an allowlist
- Says selectRaw cannot take bindings at all