You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bank::resolveOwner() prefers a caller-supplied owner name over the
authenticated session principal, and never compares the two. Ten actions across
eight models resolve their scope through it, so an authenticated client that
names another customer in the request body is served that customer's data — and
in one case writes to it.
This is a confused-deputy: the server has the authenticated identity available
and chooses the request's assertion over it.
Found while giving bank a wire-drivable server and scenario corpus
(morph#87 / PR #470). The corpus pins the current behaviour as expect ok
so the defect is visible and a fix has a failing test to turn green — it does
not endorse it.
The session principal is consulted only when the caller supplies nothing.
A non-empty action.owner is taken verbatim. The file's own header comment
says these helpers are for "reading the authenticated principal from the morph
session context" — resolveOwner is the one that defeats that.
Affected actions — 10 call sites, 8 models
file:line
action
card_model.cpp:122
ListCards
customer_model.cpp:42
OpenAccount
customer_model.cpp:62
ListAccounts
notification_model.cpp:50
ListNotifications
notification_model.cpp:80
MarkAllRead
payee_model.cpp:58
ListPayees
payment_model.cpp:140
ListPayments
statement_model.cpp:21
GenerateStatement
budget_model.cpp:72
ListBudgets
loan_model.cpp:116
ListLoans
Nine are reads. MarkAllRead is a write — it mutates another owner's
notification rows.
Two aggravating details from PR #470's investigation, to be re-verified by
whoever fixes this:
SpendingByKind consults no owner at all, so it answers a caller with no
session.
Id-addressed actions are not affected — those resolve the row first and
check its owner, which is the pattern the ten above should follow. The fix
therefore has a working in-tree model to copy rather than needing a design.
Why this matters in an example
examples/ is the ladder's teaching material — LADDER.md presents these
rungs as the reference for how a morph application is built, and bank is cited
in its intro as prior art. A scope-by-request-body pattern copied out of it
lands in real applications. The framework side is fine: morph::session
carries the authenticated principal correctly and the model simply declines to
use it.
Fix direction
Make the session principal authoritative for these ten actions. Either:
Ignore action.owner entirely and always scope to sessionPrincipal()
— simplest, and correct if no legitimate caller sets it. Check the GUI and
CLI first: if they populate owner, this changes their behaviour.
Keep the field but verify it: refuse when action.owner is non-empty
and differs from the session principal. Preserves any legitimate
administrative use, and makes the refusal explicit.
Whichever is chosen, decide deliberately what an empty session means — today
it silently scopes to "". Refusing is almost certainly right; the scenarios
pin the current answer either way.
Do not fix this by removing owner from the DTOs without checking the wire
surface, and note resolveOwner is inline in a header used by both models
and (potentially) client code.
Acceptance criteria
A scenario in which one signed-in customer names another customer and is refused — the corpus already has the shape, currently pinned expect ok;
flipping it is the test.
The same for MarkAllRead, asserting the other owner's rows are unchanged.
A decision recorded for SpendingByKind and for the empty-session case.
The existing 22 bank scenarios still pass, twice.
Out of scope
morph#87's ladder-slot question.
Bank's server trusting the client's asserted principal at all (its AuthModel
mints no token) — that is a separate, larger gap documented in PR bank: give it a server, and a scenario corpus over all 41 actions #470's
README changes. This issue is narrower: even taking the asserted principal
at face value, these ten actions ignore it.
Summary
bank::resolveOwner()prefers a caller-supplied owner name over theauthenticated session principal, and never compares the two. Ten actions across
eight models resolve their scope through it, so an authenticated client that
names another customer in the request body is served that customer's data — and
in one case writes to it.
This is a confused-deputy: the server has the authenticated identity available
and chooses the request's assertion over it.
Found while giving
banka wire-drivable server and scenario corpus(morph#87 / PR #470). The corpus pins the current behaviour as
expect okso the defect is visible and a fix has a failing test to turn green — it does
not endorse it.
The mechanism
examples/bank/include/bank/core/principal.hpp:24:The session principal is consulted only when the caller supplies nothing.
A non-empty
action.owneris taken verbatim. The file's own header commentsays these helpers are for "reading the authenticated principal from the morph
session context" —
resolveOwneris the one that defeats that.Affected actions — 10 call sites, 8 models
card_model.cpp:122ListCardscustomer_model.cpp:42OpenAccountcustomer_model.cpp:62ListAccountsnotification_model.cpp:50ListNotificationsnotification_model.cpp:80MarkAllReadpayee_model.cpp:58ListPayeespayment_model.cpp:140ListPaymentsstatement_model.cpp:21GenerateStatementbudget_model.cpp:72ListBudgetsloan_model.cpp:116ListLoansNine are reads.
MarkAllReadis a write — it mutates another owner'snotification rows.
Two aggravating details from PR #470's investigation, to be re-verified by
whoever fixes this:
SpendingByKindconsults no owner at all, so it answers a caller with nosession.
check its owner, which is the pattern the ten above should follow. The fix
therefore has a working in-tree model to copy rather than needing a design.
Why this matters in an example
examples/is the ladder's teaching material —LADDER.mdpresents theserungs as the reference for how a morph application is built, and bank is cited
in its intro as prior art. A scope-by-request-body pattern copied out of it
lands in real applications. The framework side is fine:
morph::sessioncarries the authenticated principal correctly and the model simply declines to
use it.
Fix direction
Make the session principal authoritative for these ten actions. Either:
action.ownerentirely and always scope tosessionPrincipal()— simplest, and correct if no legitimate caller sets it. Check the GUI and
CLI first: if they populate
owner, this changes their behaviour.action.owneris non-emptyand differs from the session principal. Preserves any legitimate
administrative use, and makes the refusal explicit.
Whichever is chosen, decide deliberately what an empty session means — today
it silently scopes to
"". Refusing is almost certainly right; the scenariospin the current answer either way.
Do not fix this by removing
ownerfrom the DTOs without checking the wiresurface, and note
resolveOwnerisinlinein a header used by both modelsand (potentially) client code.
Acceptance criteria
refused — the corpus already has the shape, currently pinned
expect ok;flipping it is the test.
MarkAllRead, asserting the other owner's rows are unchanged.SpendingByKindand for the empty-session case.Out of scope
AuthModelmints no token) — that is a separate, larger gap documented in PR bank: give it a server, and a scenario corpus over all 41 actions #470's
README changes. This issue is narrower: even taking the asserted principal
at face value, these ten actions ignore it.
🤖 Filed with Claude Code
https://claude.ai/code/session_01C6uB9zxdSG8qp3VNAAyFvF