Skip to content

fix(model): block SQL injection in legacy finders via adapter-side value validation#2418

Merged
bpamiri merged 1 commit into
developfrom
claude/serene-gauss-d2d326
May 3, 2026
Merged

fix(model): block SQL injection in legacy finders via adapter-side value validation#2418
bpamiri merged 1 commit into
developfrom
claude/serene-gauss-d2d326

Conversation

@bpamiri

@bpamiri bpamiri commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #2417. PR #2416 closed the SQL injection vector in the chainable QueryBuilder by validating typed values before they reach $quoteValue. The same root-cause pattern still existed in the legacy ORM paths that pre-date the chainable builder:

All three reach Base.$quoteValue (databaseAdapters/Base.cfc:476) with user-supplied values bound to integer/float/boolean columns. The adapter returns them unquoted (a known contract for the downstream RESQLWhere regex that re-extracts bare numerics into cfqueryparam). The post-hoc regex parameterizes what it sees, but doesn't strip injected OR / UNION / comment structure — so /users/1%20OR%201%3D1 produced WHERE id=1 OR 1=1, a tautology returning the first row.

Severity: HIGH (framework-level, affects every downstream Wheels app following the canonical CRUD pattern documented in our guides).

What changed

Push the validation into Base.$quoteValue itself (approach B from #2417 — the maintainer's recommended shape). One fix point, defense for any future caller.

Why adapter-side rather than caller-side

The issue flagged the audit risk for internal callers. Verified each:

Internal caller Trust Effect of adapter validation
read.cfc:170 (pagination) DB-round-tripped PK values Always valid integers — no false positives
validations.cfc:774 ($buildWhereClausePart) Model property values Non-numeric on integer column was already a DB error today; now a clearer Wheels.InvalidValue at the framework boundary
Base.cfc:66 (parameterize=false) Query plan params Malformed numerics were already broken; same — earlier failure with explicit error

No regressions in the suite (see Test plan).

Test plan

  • Local Lucee 7 + SQLite via tools/test-local.sh (with port-collision workaround for the user's daily-driver server): 3413 passed, 0 failed, 0 errored — the 9 non-passes are all in browser fixture-server specs unrelated to $quoteValue.
  • Docker Lucee 6 + SQLite, security suite: 173 passed, 0 failed, 0 errored — including 16/16 on the new LegacyFinderInjectionSpec.
  • Docker Adobe 2025 + SQLite, model suite: 804 passed, 0 failed, 0 errored (11 explicit skips for non-MySQL-only useIndex tests).
  • Docker Adobe 2025 + SQLite, security suite (with the pre-existing Adobe-incompatible McpCommandInjectionSpec temporarily set aside): 148 passed, 0 failed, 0 errored — including 16/16 on the new spec.
  • CI: full test matrix on pr.yml (Lucee 5/6/7 + Adobe 2018/2021/2023/2025 + boxlang × SQLite/MySQL/Postgres/SQL Server)

Side finding (separate fix, spawned)

vendor/wheels/tests/specs/security/McpCommandInjectionSpec.cfc calls mid(s, 7) on lines 105, 117, 133, 150 — Lucee accepts the 2-arg form, Adobe CF requires 3. Pre-existing since PR #2083 (commit db722c713); crashes the Adobe 2025 security suite at compile time. Spawned as a separate fix(test) task — out of scope for this PR.

Cross-references

  • Related fix on develop: #2416 — chainable QueryBuilder fix
  • Tracking issue: #2417

🤖 Generated with Claude Code

…lue validation (#2417)

PR #2416 closed the chainable QueryBuilder's SQL injection by validating typed
values before $quoteValue. The same root-cause pattern still existed in the
legacy ORM paths that pre-date the chainable builder:

- findByKey / updateByKey / deleteByKey via $keyWhereString (sql.cfc:1325)
- findByX / findOneByX / findAllByX via dynamic finders (onmissingmethod.cfc:165)
- $buildWhereClausePart used by validatesUniquenessOf (validations.cfc:774)

All three paths reach the adapter's $quoteValue with user-supplied values
bound to integer/float/boolean columns, where the adapter passes them through
unquoted. The downstream RESQLWhere regex re-extracts bare numerics into
cfqueryparam placeholders but leaves any injected OR / UNION / comment
structurally intact — so /users/1%20OR%201%3D1 produced WHERE id=1 OR 1=1, a
tautology returning the first row regardless of the intended key.

Push the validation into Base.$quoteValue itself so every caller (including
any future one) inherits the protection. The same regex/allowlist as #2416 —
^-?[0-9]+$ for integer, ^-?[0-9]+(\.[0-9]+)?$ for float, 0/1/true/false/yes/no
for boolean. Mismatches throw Wheels.InvalidValue. String columns are
untouched; the adapter still wraps + escapes them.

Adapter-side rather than caller-side per the audit notes in #2417: read.cfc:170
round-trips DB-stored PK values (always valid integers); validations.cfc:774
feeds model property values (a non-numeric on an integer column was already a
DB error today, now it's a clearer Wheels.InvalidValue at the framework
boundary); Base.cfc:66 fires only on parameterize=false query plans where
malformed numerics were also already broken.

Adds 16 regression tests in vendor/wheels/tests/specs/security/
LegacyFinderInjectionSpec.cfc covering tautology, UNION SELECT, comment-
truncation, and stacked-statement payloads against findByKey / updateByKey /
deleteByKey / findAllByX / findOneByX on integer + float columns, plus
accept-cases for legitimate integer/float/string values.

Closes #2417.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bpamiri
bpamiri merged commit 5860ecd into develop May 3, 2026
3 checks passed
@bpamiri
bpamiri deleted the claude/serene-gauss-d2d326 branch May 3, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: SQL injection in legacy finders (findByKey, dynamic finders) via same root-cause as #2416

1 participant