fix(controller): harden request lifecycle B1–B4 B7–B8 - #3400
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prove-red then green for controller / request-lifecycle hardener BLOCKERs B1–B4 and B7–B8.
Test-only commit
7cbe4db8was pushed first. Implementation is in9b791686.Summary
B1 —
$ensureControllerAndActionretargetQuery string, form, and JSON body could set
params.controller/params.actionafter a route had already matched.$ensureControllerAndActiononly filled those keys when they were missing, soGET /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 —
$cgiScopetrustsX-Rewrite-URL/X-Original-URLungatedBlank
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, defaultfalse). Fall through torequest_uri/redirect_url. This does not fliptrustProxyHeaderson. IIS rewrite installs that need those headers must opt in, same asX-Forwarded-*.B3 —
form._methodunrestricted including GET/HEAD$getRequestMethodalready ignored_methodon GET (issue #886). POST still accepted any_methodvalue, includingGET/HEAD.Fix: keep the POST-only override; allowlist
PUT/PATCH/DELETEonly. Documented form-helper_methodbehavior is unchanged. GET/HEAD cannot become a state-changing verb, and POST cannot become a CSRF-safe verb.B4 — before-filter
return falsedoes not haltprocessAction$runFilterstreatedreturn falseas "skip remaining filters".processActionthen still called the action unless a render/redirect already happened. An auth filter thatreturn falsewithoutredirectTo()/renderText()was fail-open.Fix: a before filter that returns
falseskips the action and after filters. Public halt viaredirectTo()/renderText()is unchanged. This is a fail-closed security gate for the previously undocumentedreturn falsepath, not a new public setting. Lead/CoS if that undocumented "skip remaining filters but still run the action" behavior must stay the default.B7 —
appendToKeysilent skip /ListLastnested path!IsDefined(item)skipped the segment, so two requests that both omitted the var shared one cache key. Nestedsession.user.idusedListFirst+ListLastand readsession.id.Fix: walk the full dotted path; throw
Wheels.KeyNotFoundwhen any segment is missing. No silent omit. No new setting.B8 — filter
typeis case-sensitivetype="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
filterChain("all")returns live mutable arrayprocessActionalways return truePROVEN / UNPROVEN
wheels test --core --ci --filter=hardener(23 pass)X-Rewrite-URL/X-Original-URLare ignored unlesstrustProxyHeaders=truetrustProxyHeadersdefault (false). Not a default flip of that setting._method; POST_methodallowlisted to PUT/PATCH/DELETE_methodvalues (GET/HEAD/TRACE).return falseskips the actionappendToKeyis walked; undefined items throw instead of collapsing keystype="Before"matches$runFilters(type="before")and is stored asbefore==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
7cbe4db8is on the branch for CI.Related suites after the fix (same driver):
wheels test --core --ci --filter=dispatch→ 135 passedwheels test --core --ci --filter=controller→ 550 passedwheels test --core --ci --filter=global→ 199 passedwheels test --core --ci --filter=security→ 290 passedHEAD
9b7916860e81abf40ca1d4c5e28c63e3138c44bcBase:
886eda8b04f41667879057994434ec1f80fafa49Test commands