fix(view): render debug bar env quick-switch links only when switching can work#3141
Conversation
…g can work Since #2082 the ?reload=<environment> switch requires a non-empty reloadPassword plus a matching password parameter (and is gated by allowEnvironmentSwitchViaUrl), but the debug bar still rendered the Testing/Maintenance/Production quick-switch anchors only when NO reloadPassword was set — the exact configuration where switching is impossible. Clicking one restarted the app and silently stayed in the current environment. The Environment panel now renders the links only when a reloadPassword is configured AND allowEnvironmentSwitchViaUrl resolves true. The links never embed the password: a new wdbEnvSwitch() handler prompts for it at click time and issues the documented ?reload=<environment>&password=... request. The plain ?reload=true reload anchor keeps its existing no-password gate. Guides (debug-panel.mdx, environments-and-configuration.mdx) updated to describe the working behavior instead of the dead-link caveat. Fixes #3060 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 inverted gate on the debug bar's environment quick-switch links (issue #3060, dead since #2082) with a well-evidenced red-green spec, accurate guide updates, and a proper changelog fragment. The approach — prompt for the reload password at click time instead of embedding it — is right, and the gate logic is sound (allowEnvironmentSwitchViaUrl is always a resolved boolean by request end per onapplicationstart.cfc). However, one reachable edge violates the PR's own "password is never embedded in the page" guarantee: baseReloadURL passes the raw cgi.query_string through, and a residual password= parameter lands inside the new data-wdb-reload attributes. Verdict: request changes — the fix is small and squarely within this PR's stated contract.
Security
Residual password= in the query string is embedded into the new quick-switch attributes — vendor/wheels/events/onrequestend/debug.cfm:317
<a href="##" data-wdb-reload="#EncodeForHTMLAttribute(local.baseReloadURL & local.ei)#" ...>local.baseReloadURL appends the raw query string (debug.cfm:20-21) and the strip loop at debug.cfm:24-30 removes only reload=<env|true> pairs — it never strips password=. The redirect that does strip reload,password,lock (onapplicationstart.cfc:474-483) only fires when redirectAfterReload is true, which defaults to false in development and testing (events/init/orm.cfm:26; auto-enabled only for production/maintenance at lines 52-54).
Reachable via the documented flow: request ?reload=development&password=pw (e.g. switching back into development). The app switches, no strip-redirect fires, the dev page renders the debug bar, and the quick-switch links carry data-wdb-reload="/&password=pw?reload=testing" — the live reload password in the page, contradicting the spec's assertion and the PR description. The same applies to an allowIPBasedDebugAccess admin switching into testing.
This exposure is new with this PR: before it, the quick-switch anchors rendered only when no password was set (nothing real to leak), and the plain reload anchor (debug.cfm:188) hides whenever a password is set — so these links are the first password-set-configuration consumers of baseReloadURL.
Note the same trace also shows a correctness byproduct: after the strip removes ?reload=development, the Contains "?" check at debug.cfm:31 misfires and the rebuilt target is malformed (/&password=pw?reload=testing), so the switch built from such a page would not even route correctly.
Proposed fix: filter the query string by key when building baseReloadURL, mirroring the existing redirect filter:
<cfif Len(cgi.query_string)>
<cfset local.cleanPairs = []>
<cfloop list="#cgi.query_string#" index="local.pair" delimiters="&">
<cfif NOT ListFindNoCase("reload,password,lock", ListFirst(local.pair, "="))>
<cfset ArrayAppend(local.cleanPairs, local.pair)>
</cfif>
</cfloop>
<cfif ArrayLen(local.cleanPairs)>
<cfset local.baseReloadURL &= "?" & ArrayToList(local.cleanPairs, "&")>
</cfif>
</cfif>This replaces the literal-value strip loop at lines 24-30 (which can then be deleted), fixes the malformed ?/& ordering, and keeps the spec's "never embedded" claim true. The key set reload,password,lock matches onapplicationstart.cfc:480.
Tests
Non-blocking observation: the spec's expect(output contains "spec-secret-pw-3060").toBeFalse() (DebugBarEnvQuickSwitchSpec.cfc) holds only because the harness's cgi.query_string is password-free — the CGI scope is read-only in a spec, as debugBarEncodingSpec.cfc:11-12 already documents for the same template. Worth a one-line comment in the spec acknowledging the vector it cannot exercise, so the assertion isn't read as covering it. Otherwise the spec is exemplary: red pre-fix on the exact defect, mirrors the established debugBarEncodingSpec.cfc harness (state save/restore, url.format handling), covers both gate dimensions plus the plain-reload-anchor contract, and avoids the cross-engine traps (no loops in finally, no catch-scope locals, hoisted state).
Everything that checked out
- Gate correctness:
$get("allowEnvironmentSwitchViaUrl")is guaranteed a real boolean at render time — the boot sentinel "must never escape$init()" and is resolved right after the settings includes (onapplicationstart.cfc:4,148-158,360-368). - URL composition:
baseReloadURLalways ends?reload=/&reload=(debug.cfm:31-36), so the JS'starget+'&password='+encodeURIComponent(pw)is well-formed on clean pages;prompt()cancel/empty are handled. - Output encoding:
data-wdb-reloadand the loop variable both safe (EncodeForHTMLAttribute, hardcoded env list). - Docs:
debug-panel.mdxandenvironments-and-configuration.mdxaccurately describe the new behavior and correctly drop the dead-link caveat; #3062's plain-reload contract correctly left untouched. - Changelog: proper
changelog.d/debugbar-env-quickswitch.fixed.mdfragment, no directCHANGELOG.mdedit. - Commit:
fix(view): …conforms to commitlint (type, header < 100 chars), explains the why, DCO sign-off present.
Once the query-string filter lands (and ideally the spec comment), this is good to merge.
Summary
The debug bar's Environment panel rendered the Testing / Maintenance / Production quick-switch anchors only when no
reloadPasswordwas set — but since #2082,?reload=<environment>only performs a switch when a non-emptyreloadPasswordis configured and a matchingpasswordparameter is supplied (additionally gated byallowEnvironmentSwitchViaUrl). So the links rendered exclusively in the one configuration where clicking them could never switch: the app restarted and silently stayed put.This PR implements the issue's option (b) — render the links when switching CAN work, without leaking the password:
vendor/wheels/events/onrequestend/debug.cfmis nowLen($get("reloadPassword")) AND $get("allowEnvironmentSwitchViaUrl")(the resolved post-events: explicit set(allowEnvironmentSwitchViaUrl=true) is indistinguishable from the default — documented override impossible #3031 boolean).data-wdb-reloadattribute; a newwdbEnvSwitch()handler prompts for the reload password at click time and navigates to the documented?reload=<environment>&password=...request. The password is never embedded in the page (spec asserts the configured password string is absent from the rendered output).?reload=truereload anchor (debug.cfm) keeps its existing no-password gate, unchanged — per the issue's acceptance criteria (its contract is tracked separately in Reload-password contract drift: empty password leaves?reload=trueopen to anonymous restarts, warm-app wrong-password attempts are never logged or rate-limited, and the boot warning misstates behavior #3062).Verify-first
Reproduced on develop @
bb98ffecdbefore coding:debug.cfm:304still gated the links onNOT Len($get("reloadPassword"))whileonapplicationstart.cfc:182-205requires a non-empty matching password to switch. The new spec's first run (pre-fix) failed exactly on the defect:Evidence (Lucee 7 Docker harness, port 63003)
vendor/wheels/tests/specs/events/DebugBarEnvQuickSwitchSpec.cfc: red pre-fix (2 fail), green post-fix (3 pass / 0 fail / 0 error)./wheels/core/tests?db=sqlite&format=json: 4448 pass / 12 fail / 0 error — the 12 failures are the toleratedinternal.testClientSpecbaseline artifacts.set(reloadPassword="smokepw"): dev homepage rendersdata-wdb-reload="/?reload=testing"(+ maintenance, production) prompt links;smokepwappears 0 times in the 1MB page;curl '/?reload=testing&password=smokepw'thenGET /returns 404 with no debug bar — the testing-environment dev-UI gate, i.e. the switch the JS builds actually works (matches the issue's control repro).reloadPassword="": no quick-switch links; plain reload anchor still renders (spec-asserted).Docs
digging-deeper/debug-panel.mdxandcore-concepts/environments-and-configuration.mdxupdated from the dead-link caveat (added by the 2026-06-11 guide audit, referencing this issue) to the working prompt-based behavior.changelog.d/debugbar-env-quickswitch.fixed.mdfragment added.Fixes #3060
🤖 Generated with Claude Code