Skip to content

Wheels (CFML) coordinated eight-GHSA wave: key-finders replace scope predicates, client-selectable test contexts, Host-poisoned boot config, and CLI/MCP injection

Source: hourly offensive-security scan of GitHub Security Advisories, 2026-10-16 (wave published 2026-10-16T06:30:24Z). Second Wheels wave in 24 hours — the Oct 15 quote-reparse SQLi leg lives on the Sept 18 query-grammar page; same-product re-sweep rule fired again, and this dump is the framework's own authorization/context/config-boundary inventory.

The recurring shape: framework convenience layers silently drop, widen, or re-select the enforcement decision the application thought it made. A findByKey() convenience finder replaces the app's scope predicate with a primary-key condition; a header or cookie switches the request into a parallel application context that carries its own rate-limit counters; the Host header of the first request that boots the app becomes a persisted production setting; nested-form children are resolved by posted key with no parent binding.

Advisory table

Leg GHSA / CVE Band Boundary
Scope-replacing key finders GHSA-r2r4-mr5f-x9qg / CVE-2026-111445 (high) 4.0.0–4.1.2, fixed 4.2.0 findByKey()/exists(key=) replace incoming scope conditions with a PK condition → apps relying on scopes for ownership/tenant authz load, update, or delete another tenant's record from a request-supplied key; exists(key=) with errors suppressed returns true for out-of-scope keys — a positive existence/masking oracle
Public dev tools by default GHSA-5pfh-pw79-rgrh / CVE-2026-111446 (high) 4.0.0–4.1.2 enablePublicComponent=true (default in dev) exposes password-less /wheels/* tools: application settings, environment/datasource details, route tables, and remotely starting test runs — which write to the real application database when tests use the primary datasource. Pre-4.1.2 no local restriction at all; 4.1.2 "restriction" trusts client-supplied headers without checking the connecting peer
Client-selectable test context GHSA-jf33-wf86-q3mp / CVE-2026-111443 (medium) 4.0.6–4.1.2 X-Wheels-Test-Context header, WHEELS_TEST_CONTEXT cookie with any value, or a test-runner path anywhere in the URL incl. the query string switches the request into a separate application context — even in production — sharing the same config/environment/datasource/debug settings. Effect: bypasses runtime-only maintenance mode, gets a second set of in-memory rate-limit counters, triggers app-startup side effects
Host-poisoned error-mail boot config GHSA-fg56-4rxr-cx5x / CVE-2026-111450 (medium) ≤4.1.1, fixed 4.1.2 Default production error-email recipient derived from the Host header of the request that starts the application. Trigger a cold start with your Host → all later error emails for that app's lifetime go to you, disclosing error/request metadata and (on engines where scope dumps render) config secrets and other users' session/cookie data; Lucee 7 limits disclosure to header fields
Nested-properties child IDOR GHSA-3qfm-8v6g-jr47 / CVE-2026-111447 (high) 1.1–4.1.2 nestedProperties() resolves hasMany/hasOne child rows by posted primary key with no parent-membership check → any user with a nested form can update/reassign/delete another parent's children (delete when allowDelete=true); nested belongsTo editable by posting another record's key; new composite-PK children retain posted key values — the request chooses the new row's identity
Inline-literal backslash SQLi GHSA-cpjx-54pq-8rcv / CVE-2026-111444 (critical) ≤4.1.2, fixed 4.2.0 MySQL/MariaDB backslash-escaping SQLi where request data reaches inline-literal paths: parameterize=false opt-in queries, IN lists, and parser-unbound positions in developer where strings — BETWEEN bounds, function arguments, LIKE ESCAPE literals. Doubling single quotes without accounting for backslash escapes splices the query. Bound paths unaffected. Cross-ref the Oct 15 ''-reparse leg — same framework, two distinct quote-grammar escapes
CLI deploy/rollback shell injection GHSA-p9x9-cr8j-4m4h / CVE-2026-111451 (high) 4.0.0–4.1.1, fixed 4.1.2 Wheels CLI deploy release/version/rollback values execute shell commands on deploy hosts (SSH user) and on the CI runner during builds; package removal/destroy deletes unintended files; package extraction overwrites other vendor files; generator names write outside the project or inject executable CFML into generated code; generated scaffold/admin views ship unencoded display properties (stored XSS). Advisory explicitly names the input sources: CI variables, branch names, packages, or an AI agent using the CLI's MCP tools
Case-insensitive containment on case-sensitive FS GHSA-55rj-mxqv-p89q / CVE-2026-111448 (low) 4.0.0–4.1.2 Canonical-path checks for zip entries, package mappings, dev assets/docs, guide images use case-insensitive comparison on a case-sensitive filesystem → an existing symlink to a case-distinct sibling directory is accepted, escaping the intended root (limited file disclosure)
Shared-scope debug race GHSA-rh8q-647v-56rc / CVE-2026-111449 (medium) 3.0.0–4.1.1 allowIPBasedDebugAccess=true (off by default): an allowed admin request flips error/debug display in shared application scope; an overlapping unauthorized request that raises an error receives full exception details incl. serialized exception data

Why this is worth an operator page

  • "Key" finders are the scope-blind door of every ORM/framework. The scope-replacement leg generalizes far than CFML: any findById/getByKey-style convenience API that takes a key and returns a row must intersect, not replace, the caller's authorization scope — contrast the scope-aware finders in the same framework the advisory says were unaffected. Audit rule: enumerate every finder/aggregate whose name pattern implies direct key access, and diff its predicate handling against the query-builder path the app's authorization depends on. Second durable leaf: exists() returning true on out-of-scope keys turns an authz bug into a standing enumeration oracle even when errors are suppressed — test exists/read/update/delete separately for the same foreign key; they routinely disagree.
  • Client-selectable application context = a parallel enforcement plane. The X-Wheels-Test-Context leg is the header/cookie-selected-route-family family (nginx-ui MCP mount, Keycloak versioned account routes) at application-instance granularity: the alternate context keeps config, datasource, and debug settings but resets in-memory state — so per-context rate-limit counters and maintenance-mode flags silently fork. Hunt rule: on any framework, grep for context/env/tenant selectors read from headers, cookies, or URL substrings (note: the trigger is a path substring anywhere in the URL, including the query string — sweep param values for internal route names), then re-run your limiter and maintenance-mode probes through the alternate context; counters that "hit the limit" on the normal path may be brand new behind the selector.
  • Boot-time derivation turns one request into persistent config. Host-header-as-error-recipient is Host-as-data again (Nezha mintTransferTool was Host-as-template-variable, DeepSeek Harness was Host-as-auth-gate — this is Host-as-persisted-setting-lifetime): the attacker-chosen value is latched by whichever request wins the cold start. Sweep on authorized assessments: send Host: attacker.tld variants during/restart-adjacent to boot, then observe any mail/webhook/config surface for the poisoned value; the read-back channel here is email, and email bodies in error-debug modes carry scope dumps (session cookies, config secrets). Same audit question for any app that derives defaults from the first request: which settings are initialized lazily from request fields?
  • Nested-form child resolution must prove parent membership. Posted-PK child lookup with no parent binding is the either-end edge-addressing family (PraisonAI dependency edges, Gitea URL-repo authorization) applied to ORM nested attributes; the composite-key variant — new rows keeping request-chosen keys — is the request-selects-identity leaf. Probe battery for any nested create/update form: post your own parent's child key (control), another tenant's child key (IDOR), a new composite key (identity selection), and a delete flag on the foreign row; diff stored state.
  • Header-trust "fixes" without peer checks are still open. The 4.1.2 dev-tools "local restriction" compares client-supplied headers instead of the TCP peer — the guard-implementation fingerprint rule: before accepting an IP/locality gate, determine whether it reads socket peer or header string (X-Forwarded-For-shaped bypass), then verify the exposure itself (/wheels/* settings/datasource/route disclosure is recon gold; remotely started tests that write the primary datasource turn a read primitive into data corruption against the real app).
  • The CLI/MCP second face of frameworks. p9x9 names AI-agent-via-MCP-tooling as an input source for values that shell out on deploy hosts and CI runners. Standing rule for agentic engagements: any framework CLI exposed as MCP tools inherits the trust its CI inputs never had — branch names, release/version strings, and package contents were already attacker-influenced in Git-hosted workflows; the CLI just stopped escaping them. Reproduce only with inert marker values in disposable repos/hosts.

Validation workflow (authorized scope only)

Lab CFML stacks and inert proofs only

Run Wheels at a vulnerable band in a disposable Lucee/OpenBD install with a throwaway datasource (never a real primary DB for the test-context probes), synthetic tenants, and your own mail sink (a mailbox you control).

  1. Fingerprint first: .cfm URL shapes, cfx_* cookies, and a GET sweep of /wheels/* (settings, environment, routes, migrator) anonymously; record whether datasource names/routes reflect. Then repeat with spoofed X-Forwarded-For: 127.0.0.1 / X-Real-IP: 127.0.0.1 to test the header-vs-peer leaf.
  2. Test-context battery: request a protected/normal route four ways — clean, X-Wheels-Test-Context: x, cookie WHEELS_TEST_CONTEXT=x, and a test-runner path fragment in a query parameter. Evidence: response-context difference, maintenance-mode verdict table, and a pre-exhausted rate bucket on the normal path that resets through the selector (synthetic limiters only).
  3. Scope-replacement probe: two lab tenants A/B; with an app that scopes by tenant, call a scoped key-finder route with B's row key from A's session: diff exists/read/update/delete verdicts (expect the advisory's replace-semantics); confirm the exists()=true oracle with an out-of-scope key under production error suppression.
  4. Nested-children matrix: own-parent child (control), foreign child key (expect denied post-fix), new composite key (check stored id), allowDelete=true + foreign child (check row state, never real deletes).
  5. Host-boot-poison: restart the lab app, first request Host: <your-sink-domain>; trigger a benign error; confirm the error mail routes to your sink; record what the body discloses at default debug settings (redact any cookies in evidence).
  6. Quote-grammar differentials: test'' (Oct 15 leg) and backslash forms test\' / \\ in login/reset/lookup identifiers plus BETWEEN/LIKE ESCAPE-adjacent params on lab app code; grammar error vs clean miss is the fingerprint. Keep payloads to error-triggering shapes, never data-exfiltrating unions against anything but your own synthetic tables.

Processed without publication (Oct 16 06:30Z wave remainder)

  • ~55 sparse WPScan-style singles: User Profile Builder role-param self-grant (wppb_add_meta_to_user_on_activation() passes role to set_role() unvalidated — role self-grant class, AVideo/SimpleForm genealogy), stored-XSS singles (SiteOrigin CSS pair, Colibri, Ultra Addons CF7, Complianz, Cooked, Mang Board content, SQL Chart Builder, NotificationX), blind SQLi singles (Vitepos, Mail Mint filters param — filter-position class covered), directory-traversal singles (Tainacan, Debloat), WP Ghost / Feedzy media:thumbnail@url / Lightweight Cookie Notice XSS singles, Tutor LMS DOM-XSS, Dokan/FluentCart/Forminator thin singles, Backuply SQLi, InstaWP add_management_page() role-name-as-capability rendering master API key (capability-name-drift + secret-render class covered), Flying Scripts/Nexter/Modula/Groundhogg/FunnelKit/Theme My Login multisite ATO/miniOrange delay-OTP-grace-flag singles, Search Regex PHP object injection (subscriber+, POP-dependent, vuldb-known class). Klarna WooCommerce unauth order-page render leaking order_key + full billing/shipping PII inline (order-token-disclosure class; PII — no publish framing).
  • Customer Email Verification for WooCommerce activation secrets via wp_rand(100) under hashids encoding — ~7-bit, reversible; token-entropy class canonical on the Sept 18 predictable-tokens page (noted there as a follow-up this run).
  • Backdrop CMS SVG upload script execution when viewed by direct URL (img-tag containment = known SVG-render-context class); ssldump fanx_fingerprint 3-byte heap overflow (parser memory-safety precedent, tool-side, no operator workflow).
  • All remaining Oct 16 03:30Z items (Frappe pair, Nezha wave, FreeRDP pack) promoted/tracked in the 04:1xZ/05:1xZ runs; updated-feed sweep after 06:31Z = Airflow p86c re-sync only.