Skip to content

Commit e89fcdd

Browse files
kennethphoughclaude
andcommitted
Enforce read-only mode on HTTP method, not just allowableActions
Dev QA caught a bypass: the allowableActions gate lives inside the base ModelController action methods, but controllers that override them (e.g. KytePasswordResetController::new) never re-check — so the mode-1 read-only intersect was silently bypassed and an anonymous POST executed. ModelController::init() now throws for any non-GET app_context request when allow_public=1, before any action method can run. The intersect stays as defense in depth for base-class actions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2f1f416 commit e89fcdd

1 file changed

Lines changed: 16 additions & 7 deletions

File tree

src/Mvc/Controller/ModelController.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,25 @@ protected function init()
194194

195195
// Anonymous app-context (JWT-mode public access via AppContextStrategy).
196196
// Per-app tri-state Application.allow_public governs the anonymous
197-
// surface: 1 = read-only (restrict to GET regardless of the
198-
// controller's allowableActions); 2 = controller-governed (the
199-
// controller's own requireAuth=false + allowableActions declaration
200-
// applies, including writes — the same contract HMAC anonymous has
201-
// always honored). allow_public=0 never reaches here (preAuth rejects).
202-
// Only fires in dispatcher mode when the anonymous strategy was
203-
// selected; every other path is unaffected.
197+
// surface: 1 = read-only (GET only, regardless of the controller's
198+
// allowableActions); 2 = controller-governed (the controller's own
199+
// requireAuth=false + allowableActions declaration applies, including
200+
// writes — the same contract HMAC anonymous has always honored).
201+
// allow_public=0 never reaches here (preAuth rejects). Only fires in
202+
// dispatcher mode when the anonymous strategy was selected; every
203+
// other path is unaffected.
204+
//
205+
// Read-only is enforced on the HTTP METHOD, not just allowableActions:
206+
// controllers that override new()/update()/delete() (e.g.
207+
// KytePasswordResetController) skip the base class's allowableActions
208+
// check, so the intersect alone is bypassable. Throwing here runs
209+
// before any action method can.
204210
if (isset($this->api->authStrategy) && $this->api->authStrategy !== null
205211
&& $this->api->authStrategy->name() === 'app_context'
206212
&& (int)($this->api->app->allow_public ?? 0) === 1) {
213+
if (strtoupper((string)$this->api->request) !== 'GET') {
214+
throw new \Kyte\Exception\SessionException("Anonymous access to this application is read-only.");
215+
}
207216
$this->allowableActions = array_values(array_intersect($this->allowableActions, ['get']));
208217
}
209218

0 commit comments

Comments
 (0)