Where: PATCH /admin/settlements/:id/status → update_settlement_status in src/handlers/settlements.rs:172-201. The request body includes an optional actor: Option<String> (settlements.rs:137), and the handler does let actor = payload.actor.as_deref().unwrap_or("admin"); (settlements.rs:187), then passes it straight through SettlementService::update_status (src/services/settlement.rs:320-357) to queries::update_settlement_status(..., actor), which persists it into the settlement's audit/history trail. The only length constraint applied is validate_max_len("actor", actor, 50) (settlements.rs:160-163) — the value itself is entirely free text supplied by the caller.
Critically, the route sits behind admin_auth (src/lib.rs:278-280), which authenticates with a single shared bearer secret (src/middleware/auth.rs:64-100) — either from SecretsStore::valid_admin_keys() or the ADMIN_API_KEY env var — with no notion of individual admin identity. There is no mechanism tying the authenticated request to a specific person/service.
Impact: any holder of the shared admin key can attribute a settlement status change (e.g. moving a disputed settlement to adjusted with a new total, or to voided) to an arbitrary actor string of their choosing — including impersonating a specific colleague, or using a generic value to hide who actually made the change. For dispute-workflow / financial audit-trail purposes, this defeats the accountability the audit log is meant to provide.
Suggested fix: derive actor server-side from the authenticated principal (once per-admin identity exists) rather than trusting a client-supplied field; at minimum, don't let the request body override it.
Where:
PATCH /admin/settlements/:id/status→update_settlement_statusinsrc/handlers/settlements.rs:172-201. The request body includes an optionalactor: Option<String>(settlements.rs:137), and the handler doeslet actor = payload.actor.as_deref().unwrap_or("admin");(settlements.rs:187), then passes it straight throughSettlementService::update_status(src/services/settlement.rs:320-357) toqueries::update_settlement_status(..., actor), which persists it into the settlement's audit/history trail. The only length constraint applied isvalidate_max_len("actor", actor, 50)(settlements.rs:160-163) — the value itself is entirely free text supplied by the caller.Critically, the route sits behind
admin_auth(src/lib.rs:278-280), which authenticates with a single shared bearer secret (src/middleware/auth.rs:64-100) — either fromSecretsStore::valid_admin_keys()or theADMIN_API_KEYenv var — with no notion of individual admin identity. There is no mechanism tying the authenticated request to a specific person/service.Impact: any holder of the shared admin key can attribute a settlement status change (e.g. moving a disputed settlement to
adjustedwith a new total, or tovoided) to an arbitraryactorstring of their choosing — including impersonating a specific colleague, or using a generic value to hide who actually made the change. For dispute-workflow / financial audit-trail purposes, this defeats the accountability the audit log is meant to provide.Suggested fix: derive
actorserver-side from the authenticated principal (once per-admin identity exists) rather than trusting a client-supplied field; at minimum, don't let the request body override it.