Reviewed commit: be11bdcb5 (origin/main), from the 2026-07-28 full-repo review.
Severity: CRITICAL. Class: the read side of a feature is scoped by allowed_accounts, the write side of the same feature is not.
Where
internal/api/handler_ladder.go:77-122 (write path, upsertLadderConfig)
internal/api/handler_ladder.go:27-66 (read path, which does call filterLadderConfigsByAllowedAccounts)
internal/api/handler_ladder.go:129 (validateLadderAccountProvider)
What
PUT /api/ladder/configs gates only on requirePermission(ctx, req, "update", "config"). cfg.CloudAccountID comes straight from the request body; validateLadderAccountProvider only checks that the account exists and that its provider matches. Neither getAllowedAccounts / MatchesAccount nor requirePermissionConstraints is called before h.config.UpsertLadderConfig at :116.
The GET handler's own doc comment (:20-26) records that migration 000088 granted view:config to the Standard User and Read-Only User groups precisely so that scoped users reach this endpoint. That reasoning was applied to the read and not to the write.
Failure scenario
Standard User U has allowed_accounts = [acct-A] and holds update:config. U sends:
PUT /api/ladder/configs
{"cloud_account_id":"<acct-B-uuid>","provider":"aws","enabled":true,
"mode":"auto_approve","cadence":"daily","max_hourly_commit_per_run":null, ...}
which returns 200. LadderConfigDB.MaxHourlyCommitPerRun is *float64 where nil means no cap (internal/config/types.go:1075-1077), and Mode accepts auto_approve, so the ladder engine now auto-purchases uncapped commitments on account B, which U has no entitlement to and cannot even see. The GET filters row B back out, so the change is invisible to U afterwards and to any other scoped operator.
Fix direction
Call h.requireAccountAccess(ctx, session, cfg.CloudAccountID) before the upsert, and add a requirePermissionConstraints("update", "config", {AccountIDs, Providers}) set, mirroring executeExchange's pattern.
Related
Same family as the other write-side scoping gaps filed from this review. Not covered by LeanerCloud/cloud-commitments-platform#70 / LeanerCloud/cloud-commitments-go#27 / LeanerCloud/cloud-commitments-go#36, which are ladder feature/algorithm issues.
Reviewed commit:
be11bdcb5(origin/main), from the 2026-07-28 full-repo review.Severity: CRITICAL. Class: the read side of a feature is scoped by
allowed_accounts, the write side of the same feature is not.Where
internal/api/handler_ladder.go:77-122(write path,upsertLadderConfig)internal/api/handler_ladder.go:27-66(read path, which does callfilterLadderConfigsByAllowedAccounts)internal/api/handler_ladder.go:129(validateLadderAccountProvider)What
PUT /api/ladder/configsgates only onrequirePermission(ctx, req, "update", "config").cfg.CloudAccountIDcomes straight from the request body;validateLadderAccountProvideronly checks that the account exists and that its provider matches. NeithergetAllowedAccounts/MatchesAccountnorrequirePermissionConstraintsis called beforeh.config.UpsertLadderConfigat:116.The GET handler's own doc comment (
:20-26) records that migration 000088 grantedview:configto the Standard User and Read-Only User groups precisely so that scoped users reach this endpoint. That reasoning was applied to the read and not to the write.Failure scenario
Standard User U has
allowed_accounts = [acct-A]and holdsupdate:config. U sends:which returns 200.
LadderConfigDB.MaxHourlyCommitPerRunis*float64where nil means no cap (internal/config/types.go:1075-1077), andModeacceptsauto_approve, so the ladder engine now auto-purchases uncapped commitments on account B, which U has no entitlement to and cannot even see. The GET filters row B back out, so the change is invisible to U afterwards and to any other scoped operator.Fix direction
Call
h.requireAccountAccess(ctx, session, cfg.CloudAccountID)before the upsert, and add arequirePermissionConstraints("update", "config", {AccountIDs, Providers})set, mirroringexecuteExchange's pattern.Related
Same family as the other write-side scoping gaps filed from this review. Not covered by LeanerCloud/cloud-commitments-platform#70 / LeanerCloud/cloud-commitments-go#27 / LeanerCloud/cloud-commitments-go#36, which are ladder feature/algorithm issues.