Skip to content

Reload-password contract drift: empty password leaves ?reload=true open to anonymous restarts, warm-app wrong-password attempts are never logged or rate-limited, and the boot warning misstates behavior #3062

Closed

Description

@bpamiri

Summary

The security-hardening and production-config guides — and the framework's own boot warning — describe a reload-password contract that the dispatch gate does not implement. Three drifts, one root cause (the onRequestStart reload gate predates the #2082 hardening work, which all landed in the cold-start path only):

  1. Empty reloadPassword does NOT disable plain reload. With set(reloadPassword=""), any anonymous client can ?reload=true and trigger a full applicationStop() restart. Only the environment-switch leg is disabled when empty (onapplicationstart.cfc:188, scoped by fix(config): require non-empty reload password for environment switching #2082). An internet-reachable app with an empty password can be restart-DoS'd in a loop.
  2. The boot warning asserts the opposite. onapplicationstart.cfc:372-374 logs: "reloadPassword is empty — URL-based environment switching and application reload are disabled until a password is set". The reload half of that sentence is false.
  3. Wrong-password attempts against a warm app are invisible and unthrottled. With a password set, ?reload=true&password=wrongpw returns HTTP 200 (the page is served normally — there is also no 403, contrary to the guide). The attempt produces no wheels_security.log entry and no rate-limit count: the logging + 5-attempts-per-5-minutes lockout (onapplicationstart.cfc:160-178, 219) only execute during application cold start; the warm-path gate in public/Application.cfc:268-288 falls through silently first. Brute-forcing the reload password against a running app is unmonitored.

Repro (develop @ f668c50, Lucee 7 docker harness)

# stage harness configs, then:
printf '<cfscript>\nset(reloadPassword="");\n</cfscript>\n' >> config/settings.cfm
docker run -d -t --memory=3g -p 60007:60007 -v "$PWD":/wheels-test-suite wheels-test-lucee7:v1.0.1

# 1. empty password: anonymous restart succeeds
curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:60007/?reload=true'   # -> 302, Location: /
# proof of restart: a route appended to config/routes.cfm before the curl goes 404 -> 200
# (also reproduced in production env: production-config audit saw 302 + /smokeprobe4 404->200)

# 2. with a password set: wrong password is silent
# set(reloadPassword="smokepw") instead, restart container, then:
curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:60007/?reload=true&password=wrongpw'  # -> 200 (no 403, no reload)
grep -c 'rejected' logs/wheels_security.log   # -> no new entry for the warm-path attempt

Root cause

The reload gate in public/Application.cfc:268-288 explicitly fires when !Len(application.wheels.reloadPassword) (... || !Len(...) || secureCompare(...)), and on mismatch falls through to normal serving with no logging, no 403, no rate-limit bump. All the hardening (#2082: non-empty-password requirement; rejected/accepted logging at onapplicationstart.cfc:219/227; per-IP rate limit at :160-178) lives only in the cold-start path. The boot warning text was written as if #2082 covered plain reload too.

Proposed direction

Decision needed first — should an empty reloadPassword disable URL reload entirely (matching the warning + both guides), or remain a dev convenience? Then:

  • If "empty = disabled": gate ?reload=true on a non-empty password in public/Application.cfc (mirror the env-switch leg); update guides to match.
  • Either way: log warm-path wrong-password attempts to wheels_security.log with the source IP and count them against the same per-IP rate limit as the cold-start path.
  • Fix the boot warning text at onapplicationstart.cfc:372-374 to state exactly what is disabled.
  • Update deployment/security-hardening.mdx ("If it's empty, both are disabled" + the nonexistent 403) and deployment/production-config.mdx ("Empty disables ?reload= entirely", settings comment + checklist item 2).

Acceptance

  • Boot warning text, both guides, and gate behavior agree on what an empty reloadPassword means.
  • If "empty = disabled": ?reload=true with empty password serves the page normally without restarting (harness: no 302, no cold start).
  • Wrong-password attempts against a warm app are logged with source IP and feed the per-IP rate limit (parity with cold-start), or the docs explicitly scope logging to cold start.
  • Specs cover: empty-password warm reload, wrong-password warm reload (log + rate-limit), correct-password warm reload.

Reported by the 2026-06-11 guide behavioral audit (P1 batch 1): security-hardening verifier (claims reload-empty-disables-claim-FALSE [both], reload-403-claim-FALSE, reload-attempts-logged-with-ip) and production-config verifier (claim empty-reloadpw-disables-reload); the env-config verifier independently confirmed the boot-warning text. Findings catalog: docs/superpowers/audits/2026-06-guide-audit-findings.md.

Activity

  1. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Triage

    Classified as bug.

    The report describes observable wrong behavior backed by repro steps and exact line references, and the defect reproduces in isolation: the warm-path reload gate at public/Application.cfc:269-280 short-circuits on \!Len(application.wheels.reloadPassword), so an empty reloadPassword lets anonymous ?reload=true trigger a restart, and on a wrong-password mismatch the warm path falls through to normal serving with no wheels_security.log entry and no per-IP rate-limit bump. The boot warning at vendor/wheels/events/onapplicationstart.cfc separately asserts the opposite. (The embedded "empty = disabled vs dev convenience" question is a design trade-off, but the logging/rate-limit gap and the misstated warning are defects regardless of how that is decided — hence bug, not framework-design.)

    Suspected layer

    config — the reload-password contract is a config/settings.cfm setting (set(reloadPassword=...)); ground the security reasoning in .ai/wheels/security/ (logging, rate-limit, $secureCompare parity with the cold-start path).

    Fix sketch

    Two framework files plus docs. In public/Application.cfc (the warm-path gate, ~lines 269-280): mirror the env-switch leg so an empty reloadPassword no longer satisfies the reload condition, and on a wrong-password mismatch log the attempt to wheels_security.log with source IP and count it against the same per-IP rate limit used in the cold-start path. In vendor/wheels/events/onapplicationstart.cfc (~lines 372-374): correct the boot-warning text to state exactly what an empty password disables. Then reconcile deployment/security-hardening.mdx (the "both are disabled" + 403 claims) and deployment/production-config.mdx so the warning, both guides, and the gate agree. The hardening logic (#2082) already exists in the cold-start path — the work is extending it to the warm path.

    Confidence: medium

    Auto-downgraded from high: the fix touches a security/auth reload gate (DoS surface, brute-force logging/rate-limiting), and the "empty password = disabled vs dev convenience" semantics is a real trade-off that must be decided before the gate change lands.

    <!-- wheels-bot:triage:3062 -->
    <!-- wheels-bot:triage-class:bug -->
    <!-- wheels-bot:triage-confidence:medium -->

  2. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Fix on hold for human review

    The proposed fix touches a sensitive area (vendor/wheels reload-password gate / security logging / rate-limiting — password + auth code) and the bot's safety net requires a human in the loop before any code is written.

    Two reasons reinforce the hold:

    1. Security/auth surface. The change lives in the warm-path reload gate (public/Application.cfc:268-288), which performs $secureCompare password matching with a direct restart-DoS and brute-force surface, plus the requested wheels_security.log + per-IP rate-limit parity work. These are exactly the categories the safety net reserves for human authorship.
    2. Unresolved design decision. The issue explicitly states "Decision needed first — should an empty reloadPassword disable URL reload entirely (matching the warning + both guides), or remain a dev convenience?" The triage comment likewise flagged this as a trade-off that must be settled before the gate change lands. The correct gate behavior — and therefore the spec — cannot be written until a maintainer picks the contract.

    A maintainer should decide the empty-password contract, then either author the fix directly or re-task the bot with the decision recorded on this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions