Skip to content

[13.x] Escape single quotes in Postgres JSON path attributes - #60923

Merged
taylorotwell merged 2 commits into
laravel:13.xfrom
kpanuragh:fix/pgsql-json-path-quote-escaping
Jul 30, 2026
Merged

[13.x] Escape single quotes in Postgres JSON path attributes#60923
taylorotwell merged 2 commits into
laravel:13.xfrom
kpanuragh:fix/pgsql-json-path-quote-escaping

Conversation

@kpanuragh

Copy link
Copy Markdown
Contributor

PostgresGrammar::wrapJsonPathAttributes() interpolates JSON path attributes into single quoted SQL string literals without escaping single quotes:

return filter_var($attribute, FILTER_VALIDATE_INT) !== false
    ? $attribute
    : $quote.$attribute.$quote;

A single quote in a JSON path therefore terminates the literal early:

DB::table('users')->select("meta->x', (select token from api_secrets limit 1) leaked, 'y")->get();
select "meta"->>'x', (select token from api_secrets limit 1) leaked, 'y' from "users"

This is inconsistent with the rest of the codebase in two ways:

  1. Every other driver escapes this. MySQL, MariaDB, SQLite and SQL Server all go through CompilesJsonPaths::wrapJsonPath(), which has applied preg_replace("/([\\\\]+)?\\'/", "''", $value) since [5.8] Correctly escape single quotes in json paths #28160. The same input is inert on those drivers. Postgres uses a separate ->/->> code path that never received the equivalent treatment.

  2. This grammar already escapes it elsewhere. PostgresGrammar::compileJsonContainsKey() uses str_replace("'", "''", $lastSegment), which is exactly the escaping applied here.

I checked the sinks that reach wrapJsonPathAttributes()select, addSelect, where/orWhere, whereIn, whereNull, whereNotNull, whereBetween, whereDate, orderBy, groupBy, having, join, whereJsonContains, whereJsonLength, update and upsert are all affected. whereJsonContainsKey is the only one that was already safe, via the str_replace above.

Note that plain (non-JSON) column names are not affected — identifier wrapping already neutralises those, e.g. orderBy('id, (select ...)') compiles to order by "id, (select ...)" asc. The JSON path is the only place where the quoting is escaped.

On the update path

compileJsonUpdateColumn() passes " as the attribute delimiter, but the resulting path is still nested inside a '{...}' string literal:

update "users" set "meta" = jsonb_set("meta"::jsonb, '{"role"}', ?)

So escaping only the configured delimiter leaves that sink open. This PR escapes single quotes unconditionally, and additionally doubles the delimiter when it isn't a single quote.

Tests

Added testPostgresJsonPathEscaping and testPostgresUpdateJsonPathEscaping, mirroring the existing testJsonPathEscaping coverage added in #28160. Both fail on the current 13.x and pass with this change. The full tests/Database suite passes (2776 tests, 7659 assertions).

I appreciate that the documentation advises against letting user input dictate column names, and I'd echo the advice given on #28160 that column names should still be whitelisted. This is submitted as a consistency fix so that the JSON path behaves the same across all supported drivers.

kpanuragh and others added 2 commits July 29, 2026 17:34
`PostgresGrammar::wrapJsonPathAttributes()` interpolates JSON path
attributes into single quoted SQL string literals without escaping single
quotes, so a quote in a JSON path terminates the literal early.

The equivalent path is already escaped for every other driver by
`CompilesJsonPaths::wrapJsonPath()` (laravel#28160), and within this same grammar
by `compileJsonContainsKey()`, which uses `str_replace("'", "''", ...)`.

`compileJsonUpdateColumn()` delimits path attributes with double quotes but
still nests the result inside a `'{...}'` literal, so single quotes are
escaped regardless of the configured delimiter.
@taylorotwell
taylorotwell merged commit 344fcc3 into laravel:13.x Jul 30, 2026
53 checks passed
taylorotwell added a commit that referenced this pull request Aug 14, 2026
…#61192)

* [13.x] Escape single quotes in Postgres JSON path attributes

`PostgresGrammar::wrapJsonPathAttributes()` interpolates JSON path
attributes into single quoted SQL string literals without escaping single
quotes, so a quote in a JSON path terminates the literal early.

The equivalent path is already escaped for every other driver by
`CompilesJsonPaths::wrapJsonPath()` (#28160), and within this same grammar
by `compileJsonContainsKey()`, which uses `str_replace("'", "''", ...)`.

`compileJsonUpdateColumn()` delimits path attributes with double quotes but
still nests the result inside a `'{...}'` literal, so single quotes are
escaped regardless of the configured delimiter.

* Update PostgresGrammar.php

---------

Co-authored-by: Anuragh K.P <36616831+kpanuragh@users.noreply.github.com>
Co-authored-by: Taylor Otwell <taylor@laravel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants