fix: anti-pattern-11 url shadowing in $buildRedirectUrl broke every Adobe reload (#3053) - #3057
Conversation
…3053) PR #3036 added unscoped URL-scope reads (StructKeyExists(url, "reload"), url.reload, ...) inside $buildRedirectUrl(), which had always declared a string local named url. On Adobe CF unscoped name resolution finds the local before the URL scope, so every password-gated reload, URL environment switch, and 'wheels reload' dereferenced a string and returned HTTP 500 before applicationStop() — CLAUDE.md anti-pattern #11 (reserved scope names). Lucee was unaffected because the url scope always wins there. - Rename local.url to local.redirectPath in all four same-lineage copies of public/Application.cfc (repo demo app, CLI app template, starter-app and tweet examples); full-file audit found no other reserved-scope locals or arguments. - ReloadEnvironmentSwitchParitySpec gains a fifth it-block per copy that pins the rename and fails if any local/var named url reappears in these files (line-anchored scan, comment lines skipped). - Probe-gap closure: tools/ci/smoke-env.sh probe 6 asserts an authorized reload (correct password) answers 302, not 5xx/200. Opt-in via SMOKE_RELOAD_PASSWORD; SKIPs when unset. Wired into both smoke-env.yml matrix legs with the CI app's reloadPassword. This is the probe that would have caught #3053 on an Adobe leg. Fixes #3053 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR: This PR fixes the Adobe-only HTTP 500 on every password-gated reload (#3053) by renaming the reserved-scope local local.url → local.redirectPath in $buildRedirectUrl() across all four same-lineage copies of public/Application.cfc, adds a structural spec guard pinning the rename, and closes the probe gap with an opt-in smoke probe asserting an authorized reload answers 302. The diagnosis is correct (CLAUDE.md anti-pattern #11 — unscoped name resolution finds local.url before the URL scope on Adobe CF), the fix is minimal and complete, and the RED→GREEN evidence on both Adobe 2023 and Lucee 7 is thorough. Verdict: comment — one non-blocking test-hardening nit below; everything else checks out.
Tests
-
Guard doesn't cover argument declarations named
url—vendor/wheels/tests/specs/cli/ReloadEnvironmentSwitchParitySpec.cfc:254-255:reFindNoCase("local\.url[^a-z0-9_]", line & " ") > 0 || reFindNoCase("var[ #Chr(9)#]+url[ #Chr(9)#=;]", line & " ") > 0This catches
local.urlandvar urldeclarations, but a future signature change likepublic string function $buildRedirectUrl(string url)would shadow the unscoped URL-scope reads just as fatally (theargumentsscope also precedes theurlscope in unscoped resolution) without tripping the guard. The spec comment at line 234 says "No local/var declaration (or any other use) named url" — the parenthetical slightly overstates what the regex matches. Suggested follow-up (fine in this PR or a later one): add a third pattern for parameter declarations onfunction-bearing lines, or simply tighten the comment to match the implemented coverage. Non-blocking — the PR body's manual audit confirmed no such parameter exists today.
What I verified (and found clean)
- Correctness: The rename is applied consistently in all four copies (
public/Application.cfc:469-543, CLI template, starter-app, tweet);grep -nE 'local\.url[^a-zA-Z0-9_]|var[ \t]+url[ \t=;]'across all four files returns nothing. The intentional unscoped URL-scope reads (StructKeyExists(url, "reload")atpublic/Application.cfc:447,504) are untouched and correctly not flagged by the new guard's patterns. - Cross-engine: The new spec it-block follows the exact prior-art pattern of the existing four (IIFE loop-variable capture at lines 75/268,
containsoperator,##escapes in string literals per the CLAUDE.md test gotcha). NoLeft(str, 0)risk (empty lines are skipped beforeLeft(line, 2)), theforloop is not inside afinally(Lucee 7 invariant #12), andListToArray(content, Chr(10), true)preserves empty lines so reported offender line numbers stay accurate. - Smoke probe:
tools/ci/smoke-env.sh:93-102mirrors the existing probe 4/5 style (set -uscript, explicit000handling, password masked as***in FAIL output), is correctly opt-in and last (the restart can't perturb earlier probes).SMOKE_RELOAD_PASSWORD: "wheels-dev"in.github/workflows/smoke-env.yml:47matchesset(reloadPassword = "wheels-dev")atconfig/settings.cfm:29, and the workflow'spathsfilters (public/**,tools/ci/smoke-env.sh, the workflow itself) guarantee both matrix legs exercise the new probe on this PR. - Docs: Changelog fragment present at
changelog.d/3053-buildredirecturl-url-shadowing.fixed.md(correct<slug>.fixed.mdform, no directCHANGELOG.mdedit). The shadowing class is already documented as CLAUDE.md anti-pattern #11, so no further doc update is required. - Commits: Single commit
efdd2bfe7—fix:type from the allowlist, subject ≤ 100 chars, body explains the why, DCO sign-off present. - Security: No injection surface (the probe URL uses a non-secret CI password by design, acknowledged in the workflow comment); the spec is read-only structural scanning.
…ites from behavioral audit (#3068) Audit of deployment/production-config.mdx against develop (Lucee 7 + Adobe 2023 harness) found seven docs-wrong claims and a batch of stale source citations. All corrections re-verified against the current develop head (post-#3057/#3058 line shifts). - Empty reloadPassword does NOT disable ?reload= — only URL env-switching; a bare ?reload=true still restarts the app unauthenticated (refs #3062) - wheels dbmigrate latest -> wheels migrate latest (no dbmigrate verb) - csrfStore defaults to "session" unconditionally; cookie storage is an explicit opt-in (checklist 5 parenthetical was false) - flash storage selection cite: orm.cfm:57-64, not security.cfm:49-55 - redirectAfterReload cite: orm.cfm:26/:52-54 (also flips for maintenance), not security.cfm:43-45 - wheels doctor scope: structure/files/permissions/datasource only — it does not audit the production checklist items - Refresh all stale line refs (settings cascade, env-switch resolver, migrate-down gate, settings table, dotenv step 5, secure compare, rate limit, maintenance page, boot warning, auto-migrate gate) verify:docs passes (3 tagged blocks, 0 failed). Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ve case-sensitive filesystems (#3071) The reload gate in onRequestStart() and the shared-application-name branch in onSessionStart() dispatch their handlers through $simpleLock with componentReference = "application" — a component PATH that Global.cfc's $invoke hands to cfinvoke. On case-sensitive filesystems Adobe CF resolves CFC names by exact case then all-lowercase, so the lowercase literal never matches Application.cfc and every authorized reload (?reload=true&password=...) returns HTTP 500 "Could not find the ColdFusion component or interface application" — in EVERY environment, development included. Lucee resolves case-insensitively and was unaffected, and macOS bind mounts are case-insensitive, which is why #3057's local verification and the Lucee smoke legs stayed green while the #3051 Adobe smoke legs (Linux runners) caught it. Fix: case-exact "Application" literal in all four same-lineage copies (repo demo app, CLI app template, starter-app and tweet examples), pinned by a sixth it-block in ReloadEnvironmentSwitchParitySpec (line-anchored, comment-skipping, case-sensitive scan). Verified: pre-fix 500 reproduced on a case-sensitive Docker volume in testing, production AND development; post-fix all six smoke probes pass in testing + production on the same volume, case-insensitive Adobe control unaffected, Lucee control green, full Lucee core suite at baseline (4409 pass / 12 tolerated testClientSpec artifacts). Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes #3053.
Root cause — CLAUDE.md Anti-Pattern 11 (reserved scope names)
#3036 (a6df6be) added unscoped URL-scope reads (
StructKeyExists(url, "reload"),IsBoolean(url.reload), ...) insidepublic/Application.cfc::$buildRedirectUrl(), which has always declared a string local namedurl. On Adobe CF, unscoped name resolution findslocal.urlbefore the URL scope, so the new checks dereferenced a string and threwdereference a scalar variable of type class java.lang.String— HTTP 500 beforeapplicationStop(). Every password-gated reload (?reload=true&password=...), URL environment switch (?reload=<env>&password=...), andwheels reload(drives the same URL) was broken on Adobe since the merge. Lucee was unaffected (theurlscope always wins there).Changes
local.url→local.redirectPathin$buildRedirectUrl()across all four same-lineage copies:public/Application.cfc,cli/lucli/templates/app/public/Application.cfc,examples/starter-app/public/Application.cfc,examples/tweet/public/Application.cfc. Full-file audit found no other local/argument named after a reserved scope in any copy (checkedonRequestStartand$handleRestartAppRequesttoo — they read theurlscope but declare no shadowing locals).ReloadEnvironmentSwitchParitySpecgains a fifth it-block per copy that pinslocal.redirectPathand fails if anylocal.url/var urldeclaration (or use) reappears anywhere in these files — line-anchored scan, comment lines skipped (anti-pattern 14).tools/ci/smoke-env.shprobe 6 — "authorized reload restarts cleanly": withSMOKE_RELOAD_PASSWORDset,GET /?reload=true&password=$SMOKE_RELOAD_PASSWORDmust answer 302 (not 5xx, not 200); SKIP log line when unset. WiredSMOKE_RELOAD_PASSWORD: "wheels-dev"(the CI app'sreloadPasswordfromconfig/settings.cfm) into bothsmoke-env.ymlmatrix legs. This probe turns the Adobe CF: every password reload and URL env switch returns HTTP 500 — local.url shadows the url scope in $buildRedirectUrl (#3036 regression) #3053 failure class into a red CI leg the moment an Adobe smoke leg exists (smoke-env: add an Adobe engine leg so the #3029 failure class is CI-gated #3047).RED → GREEN evidence (local docker harness, sqlite)
Adobe 2023 (wheels-test-adobe2023:v1.0.1), development env
/?reload=true&password=smokepwdereference a scalar variable ... java.lang.String, stackBUILDREDIRECTURL <- HANDLERESTARTAPPREQUESTLocation: /(params stripped), app restarts, root 200 after/?reload=testing&password=smokepwreload+password; chain terminates;/wheels/infoflips 200 → 404 (switch applied)wheels.tests.specs.cli.ReloadEnvironmentSwitchParitySpec→ 20/20 pass (5 it-blocks × 4 copies, includes the new guard).smoke-env.shvs Adobe in testing: withoutSMOKE_RELOAD_PASSWORD→ probes 1–5 PASS, probe 6 SKIP, exit 0. WithSMOKE_RELOAD_PASSWORD=smokepw→ all 6 PASS (probe 6: 302), exit 0. On unfixed develop probe 6 would have got the 500 → FAIL.Lucee 7 control (wheels-test-lucee7:v1.0.0)
/?reload=true&password=smokepw→ 302Location: /, root 200 after restart (unchanged behavior)./?reload=testing&password=smokepw→ 302 preserving params, chain terminates,/wheels/info→ 404 (switch applied, no loop).smoke-env.shin testing: probe 6 SKIP without the var (exit 0), all 6 PASS with it (exit 0).internal.testClientSpeccontainer artifacts.Spec guard RED check
Injected
local.url = "spec-red-probe";into the tweet copy → parity bundle 19 pass / 1 fail, failing spec named exactlynever shadows the url scope with a local named url in examples/tweet/public/Application.cfc. Reverted → 20/20.Notes
bash -n tools/ci/smoke-env.shclean; probe 6 is last in the script because it genuinely restarts the app.🤖 Generated with Claude Code