Update dependency kysely to v0.28.17 [SECURITY]#472
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
0.28.14→0.28.17Kysely: JSON-path traversal injection via unsanitized path-leg metacharacters in
JSONPathBuilder.key()/.at()CVE-2026-44635 / GHSA-pv5w-4p9q-p3v2
More information
Details
Summary
Kysely 0.28.12 added a
sanitizeStringLiteral()call insideDefaultQueryCompiler.visitJSONPathLeg(commit0a602bf, PR #1727) to fix CVE-2026-32763 (GHSA-wmrf-hv6w-mr66). The fix only doubles single quotes ('→''); it does not escape JSON-path metacharacters (.,[,],*,**,?). When attacker-controlled input flows intoeb.ref(col, '->$').key(input)or.at(input)— including type-safe code where the JSON column is shaped likeRecord<string, T>soK extends stringis the inferred type — every dot becomes a path-leg separator, letting an attacker traverse from the intended key into sibling and child fields the developer never meant to expose. The result is read access (and, in update statements, write access) to JSON sub-fields outside the intended scope across MySQL, PostgreSQL->$/->>$, and SQLite.kysely); affects MySQL, PostgreSQL->$/->>$, and SQLite dialects.kysely-org/kysely@master(73192e4, version0.28.16).kysely@0.28.16from npm.src/query-compiler/default-query-compiler.ts(lines 1611–1639, 1821–1823)src/query-builder/json-path-builder.ts(lines 93–196)src/dialect/mysql/mysql-query-compiler.ts(overridessanitizeStringLiteralbut inherits the same behaviour for path legs — escapes\and', nothing else)Vulnerable code
src/query-compiler/default-query-compiler.ts:1625-1639:src/query-compiler/default-query-compiler.ts:1821-1823:with
LIT_WRAP_REGEX = /'/g.src/query-builder/json-path-builder.ts:151-167:src/query-builder/json-path-builder.ts:169-196:At (1) the compiler emits the path-leg separator —
.for member access or[for array index. At (2) the user-supplied string is run throughsanitizeStringLiteral, which at (3) only doubles single quotes ('). Dots, brackets, asterisks, double-asterisks and question marks — every reserved character of the SQL/JSON path mini-language — pass through unmodified.At (4)
.key(K)typesKaskeyof NonNullable<O> & string. When the JSON column is typed asRecord<string, T>(a common shape for free-form metadata blobs) the inferredKis juststring, so attacker-controlled input is type-safe and does not need aKysely<any>escape hatch — this finding is broader thanGHSA-wmrf-hv6w-mr66(CVE-2026-32763), which only covered theKysely<any>case. At (5)/(6) the runtime accepts anystring | numberregardless oflegType, so a string sent into.at(...)('last'/'#-N'per the public type signature) also reaches the same emitter and can carry]to break out of the bracket.The fix at
0a602bfonly addressed the single-quote → string-literal escape. The JSON-path metacharacter set was overlooked.MysqlQueryCompiler.sanitizeStringLiteral(src/dialect/mysql/mysql-query-compiler.ts:47-51) overrides the helper to also escape backslashes — but again, it does nothing for. [ ] * ** ?.Reproduction (validated locally)
Environment:
kysely@0.28.16+better-sqlite3@​12.x, Node 22, on macOS. The PoC harness lives in/Users/admin/joplin_research/kysely-poc/.Step 1 — Compiled-SQL evidence across all three dialects
/Users/admin/joplin_research/kysely-poc/poc.mjs(no DB, just.compile()):The compiled SQL clearly shows the dot inside the user-supplied "key" being interpreted by the database as a path separator:
'$.nick'(one leg) becomes'$.nick.secret_field'(two legs). MySQL additionally accepts*as a wildcard reaching every member at the current level.Step 2 — End-to-end data disclosure on a real database
/Users/admin/joplin_research/kysely-poc/sqlite-runtime.mjssimulates a typical handler that reads one top-level field of the caller's profile:The
me.profileJSON column for user 1 is:{ "nick": "alice", "tagline": "hi", "internal": { "ssn": "111-11-1111", "token": "tok_abcdef", "admin": true } }The developer's intent: only top-level keys (
nick,tagline) are ever requested.internalis private bookkeeping.Expected vs. actual: the application invariant was "the user can only read top-level keys of their profile". The output violates that invariant —
internal.ssn,internal.token, andinternal.adminare returned even thoughinternalwas never meant to be addressable through this endpoint.The same pattern is exploitable on MySQL (where
*and**wildcards make it strictly worse — a single*enumerates every sibling at the current level in one row) and on PostgreSQL when using the->$/->>$operators (which target MySQL-style JSON-path strings on PG ≥ 17 / viajsonb_path_query).Impact
Record<string, T>— leaks data the developer believed was scoped behind the explicitly-listed key. SSNs, tokens, admin flags, internal IDs, anything stored as a nested member of the same JSON document is reachable.->$.key('*')compiles to'$.*', returning the array of every value at the current depth in one round-trip.key('**')recurses across the whole document. The fix does not strip either token.update().set(eb => eb.ref(col, '->$').key(input), value)-style writes (andjsonb_sethelpers). An attacker who can drive both the path and the value can therefore write into nested fields they should not be able to set — for example flipping anadminflag or rewriting a nested role.0a602bf(PR #1727) specifically to harden this surface. That fix removed the'(quote) primitive but left every JSON-path metacharacter alone, so the surface is still open against any caller that thought it was now safe..key(...)or.at(...). This is a recognised pattern (filter-by-field, dynamicselectfor admin dashboards, Strapi-style JSON-blob columns); it is not a default kysely behaviour but is plausibly common. The vulnerable path is also exercised any time a developer writesdb as Kysely<any>(covered by the olderGHSA-wmrf-hv6w-mr66advisory) — but unlike that advisory, the bug here triggers in fully-typed code onRecord<string, T>columns.Suggested fix
Treat path legs as a structured emission, not a string-literal escape. The narrowest safe patch is a dedicated
sanitizeJSONPathLegthat only emits a known-good character set per leg type and rejects everything else, since JSON-path quoting differs by dialect (MySQL allows"…"-quoted member names; SQLite is more permissive but still has a grammar; PostgreSQLjsonpathis strict).For dialect-specific behaviour (MySQL
"…"-quoted members, SQLite bracket-quoted), each dialect compiler should override the helper and apply the appropriate quoting + double-the-quote rule, the same waysanitizeIdentifieralready does.Consider also: parameterise JSON paths whenever the dialect supports it (PostgreSQL
jsonb_path_query($1, $2), MySQLJSON_EXTRACT(?, ?)), so attacker-controlled keys are bound, not concatenated. Add a regression test totest/node/src/json-traversal.test.tsasserting thateb.ref('c','->$').key('a.b').compile().sqlis either rejected, or emits MySQL'$."a.b"'/ SQLite'$.["a.b"]'(quoted-member form), and explicitly differs fromkey('a').key('b').A backstop hardening: tighten the
.at()runtime to accept onlynumber | 'last' | '#-${digits}'(matching the type signature), and tighten.key()to only accept strings that matchkeyof Oat runtime whenOis statically known.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
kysely-org/kysely (kysely)
v0.28.17: 0.28.17Compare Source
Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
0.29 is right around the corner. Try the latest RC version!
🚀 Features
🐞 Bugfixes
.key(...)and.at(...)against SQL injections and exfiltrations. by @igalklebanov in #1804📖 Documentation
📦 CICD & Tooling
🐤 New Contributors
What's Changed
Full Changelog: kysely-org/kysely@v0.28.16...v0.28.17
v0.28.16: 0.28.16Compare Source
Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
0.29 is getting closer btw. 🌶️
🚀 Features
🐞 Bugfixes
FilterObjectallows any defined value when query context has no tables (TBisnever). by @igalklebanov in #1791📖 Documentation
db646ac559714469989155a0f14b📦 CICD & Tooling
2301610f4f1d9eab6d00e521156bverifyDepsBeforeRunto "prompt". by @igalklebanov in20548bc🐤 New Contributors
What's Changed
Full Changelog: kysely-org/kysely@v0.28.15...v0.28.16
v0.28.15: 0.28.15Compare Source
Hey 👋
The introduction of dehydration in JSON functions/helpers caused an unexpected bug for consumers that have some columns defined as
'${number}', e.g.'1' | '2'(also when wrapped inColumnTypeor similar). Such columns, when participating in a JSON function/helper would dehydrate tonumberinstead of staying asstring.Why dehydrate numeric strings to numbers in the first place? Select types in
kyselydescribe the data after underlying driver's (e.g.pg) data transformation. Some drivers transform numeric columns to strings to be safe. When these columns participate in JSON functions, they lose original column data types - drivers don't know they need to transform tostring- they return as-is.This release introduces a special helper type that wraps your column type definition and tells
kyselyto NOT dehydrate it in JSON functions/helpers.🚀 Features
NonDehydrateable<T>to allow opt-out from dehydration in JSON functions/helpers. by @igalklebanov in #1697🐞 Bugfixes
PostgreSQL 🐘
📖 Documentation
📦 CICD & Tooling
🐤 New Contributors
Full Changelog: kysely-org/kysely@v0.28.14...v0.28.15
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Enabled.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.