Skip to content

fix(controller): harden request lifecycle B1–B4 B7–B8 - #3400

Merged
bpamiri merged 2 commits into
developfrom
cursor/controller-hardener-b1-b4-74ae
Aug 24, 2026
Merged

fix(controller): harden request lifecycle B1–B4 B7–B8#3400
bpamiri merged 2 commits into
developfrom
cursor/controller-hardener-b1-b4-74ae

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Prove-red then green for controller / request-lifecycle hardener BLOCKERs B1–B4 and B7–B8.

Test-only commit 7cbe4db8 was pushed first. Implementation is in 9b791686.

Summary

B1 — $ensureControllerAndAction retarget

Query string, form, and JSON body could set params.controller / params.action after a route had already matched. $ensureControllerAndAction only filled those keys when they were missing, so GET /posts?controller=admin&action=delete (or a JSON body with the same keys) retargeted the routed action.

Fix: when the matched route names a controller/action (Mapper already deletes those keys for [controller] / [action] placeholders), the route wins. Wildcard path-derived controller/action is unchanged. This is fail-closed for routed requests and does not add a new setting.

B2 — $cgiScope trusts X-Rewrite-URL / X-Original-URL ungated

Blank path_info (IIS) copied client-supplied rewrite headers into the request path with no trust gate.

Fix: honor those headers only when set(trustProxyHeaders=true) (existing setting, default false). Fall through to request_uri / redirect_url. This does not flip trustProxyHeaders on. IIS rewrite installs that need those headers must opt in, same as X-Forwarded-*.

B3 — form._method unrestricted including GET/HEAD

$getRequestMethod already ignored _method on GET (issue #886). POST still accepted any _method value, including GET / HEAD.

Fix: keep the POST-only override; allowlist PUT / PATCH / DELETE only. Documented form-helper _method behavior is unchanged. GET/HEAD cannot become a state-changing verb, and POST cannot become a CSRF-safe verb.

B4 — before-filter return false does not halt processAction

$runFilters treated return false as "skip remaining filters". processAction then still called the action unless a render/redirect already happened. An auth filter that return false without redirectTo() / renderText() was fail-open.

Fix: a before filter that returns false skips the action and after filters. Public halt via redirectTo() / renderText() is unchanged. This is a fail-closed security gate for the previously undocumented return false path, not a new public setting. Lead/CoS if that undocumented "skip remaining filters but still run the action" behavior must stay the default.

B7 — appendToKey silent skip / ListLast nested path

!IsDefined(item) skipped the segment, so two requests that both omitted the var shared one cache key. Nested session.user.id used ListFirst + ListLast and read session.id.

Fix: walk the full dotted path; throw Wheels.KeyNotFound when any segment is missing. No silent omit. No new setting.

B8 — filter type is case-sensitive

type="Before" was stored as-is. $runFilters(type="before") then depended on engine == case rules.

Fix: store and compare a canonical lowercase type. No new setting.

Out of scope

  • empty cache after redirect-only
  • filterChain("all") returns live mutable array
  • processAction always return true

PROVEN / UNPROVEN

ID Claim Status Filter / evidence
B1 Untrusted query/form/JSON cannot retarget a routed controller/action; wildcard path-derived names still work PROVEN green wheels test --core --ci --filter=hardener (23 pass)
B2 X-Rewrite-URL / X-Original-URL are ignored unless trustProxyHeaders=true PROVEN green same. Uses existing trustProxyHeaders default (false). Not a default flip of that setting.
B3 GET/HEAD ignore _method; POST _method allowlisted to PUT/PATCH/DELETE PROVEN green same. GET ignore was already closed (#886); remaining hole was unrestricted POST _method values (GET/HEAD/TRACE).
B4 Before-filter return false skips the action PROVEN green same. Fail-closed halt, not an opt-in setting. Flagged for Lead/CoS if the old "skip remaining filters only" behavior must remain the default.
B7 Nested appendToKey is walked; undefined items throw instead of collapsing keys PROVEN green same
B8 type="Before" matches $runFilters(type="before") and is stored as before PROVEN green same. Lucee == was already case-insensitive; storage is now canonical so BoxLang cannot fail-open.

Local red counts were not captured (CLI/server were not available until after the fix commit). Prove-red commit 7cbe4db8 is on the branch for CI.

Related suites after the fix (same driver):

  • wheels test --core --ci --filter=dispatch → 135 passed
  • wheels test --core --ci --filter=controller → 550 passed
  • wheels test --core --ci --filter=global → 199 passed
  • wheels test --core --ci --filter=security → 290 passed

HEAD

9b7916860e81abf40ca1d4c5e28c63e3138c44bc

Base: 886eda8b04f41667879057994434ec1f80fafa49

Test commands

wheels test --core --ci --filter=hardener
# 23 passed (0.14s)

wheels test --core --ci --filter=dispatch
# 135 passed (0.97s)

wheels test --core --ci --filter=controller
# 550 passed (2.20s)

wheels test --core --ci --filter=global
# 199 passed (0.35s)

wheels test --core --ci --filter=security
# 290 passed (0.56s)
Open in Web Open in Cursor 

Failing specs for untrusted controller/action retarget, ungated
rewrite headers, unrestricted _method including GET/HEAD, before
filter return false fail-open, appendToKey key collapse, and
case-sensitive filter type.

Co-authored-by: Peter Amiri <peter@alurium.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Route-named controller/action win over query/form/JSON. Gate IIS
rewrite headers behind trustProxyHeaders. Allowlist POST _method
to PUT/PATCH/DELETE. Halt processAction when a before filter
returns false. Walk appendToKey paths and throw on missing
segments. Normalize filter type case.

Co-authored-by: Peter Amiri <peter@alurium.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
@github-actions github-actions Bot added the docs label Aug 24, 2026
@bpamiri
bpamiri marked this pull request as ready for review August 24, 2026 11:05
@bpamiri
bpamiri merged commit ba1ac43 into develop Aug 24, 2026
20 checks passed
@bpamiri
bpamiri deleted the cursor/controller-hardener-b1-b4-74ae branch August 24, 2026 11:05
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.

2 participants