Repository navigation
Description
Summary
The /api/v1/customers and /api/v1/sales read endpoints are reachable by the ReadOnly role, exposing full customer PII and complete order financials. This contradicts the documented role model (ReadOnly = stock/production/dashboards only) and is inconsistent with the sibling money reads (/customers/balances, /payments), which are correctly gated to SalesAccess.
Severity: Medium — authenticated, same-tenant, read-only; reachable by an intentionally low-trust provisioned account (e.g. external bookkeeper/auditor).
Affected endpoints
Route groups mounted with bare .RequireAuthorization() — default policy admits any non-Denied principal, including ReadOnly (AuthPolicies.cs:34-37):
GET /api/v1/customers—ListCustomers,web...CustomerEndpoints.cs:21GET /api/v1/customers/{id}—GetCustomer,CustomerEndpoints.cs:25GET /api/v1/sales—ListSalesOrders,SaleEndpoints.cs:63GET /api/v1/sales/{id}—GetSalesOrder,SaleEndpoints.cs:59
Group mounting: Program.cs:411-421.
Exposed data
CustomerResponse(CustomerEndpoints.cs:82-83):Name, Phone, Email, Address, Note— customer PII.SalesOrderResponse(SaleEndpoints.cs:289-309):TotalMinorUnits+ per-lineUnitPriceMinorUnits— order financials.- List endpoints page up to 500 rows per call.
Why this is a real gap (not a hardening opinion)
The repo defines an explicit role contract in three places and enforces it on the derived money views but not the underlying data:
AuthPolicies.cs:15— "ReadOnly → reads only: stock, production reports, dashboards"specs/product/specs.md:464— "Read-only | View dashboards/reports only"specs/product/GLOSSARY.md:307-310— "Read-only sees stock, history, and reports only … The SPA hides gated controls; the API returns 403 with a problem body regardless" ← false for these four reads.PaymentEndpoints.cs:21,51—ListOrderPayments/ListCustomerBalancesrequireSalesAccess;RoleMatrixTests.cs:202asserts ReadOnly gets 403 on/customers/balances.- SPA hides Customers/Sales nav from ReadOnly (
web/src/routes/AppLayout.tsx:32-33) with a comment claiming the API enforces regardless.
Bare-RequireAuthorization() groups are described in AuthPolicies.cs:29 as "the open production READS" — customers/sales are not production reads; the pattern leaked past its intent.
Exploit scenario
- Owner provisions a ReadOnly user for a low-trust reviewer, expecting stock/production visibility only.
- That user, with their normal token, calls
GET /api/v1/customers?limit=500andGET /api/v1/sales?limit=500. - They enumerate the tenant's entire customer contact list (names, phones, emails, addresses) and every order's totals and line pricing — data explicitly outside their authorized scope.
Exposure is tenant-scoped (TenantContext), so no cross-tenant leak; the violation is the ReadOnly tier within a tenant.
Fix
Apply .RequireAuthorization(AuthPolicies.SalesAccess) to the customer and sales-order read endpoints (list + get), matching the gating on balances/payments:
- If Workers must retain these reads for the sell flow, use
SalesFlowinstead (still excludes ReadOnly). - Extend
RoleMatrixTeststo assert ReadOnly receives 403 onGET /api/v1/customersandGET /api/v1/sales.
Docs to sync (per repo rule)
If the intended model changes, update GLOSSARY.md and the SPA Help page (#71) in the same PR.
Filed from a whole-repo security review of main (78d25b7).
Activity
F127: hide dashboard sales panel for ReadOnly; add by-id read guards
test(e2e): Playwright smoke suite for the SPA over the #243 simulatio…
feat(sim): canary-under-load UX probe recording Core Web Vitals along…
ci(e2e): workflow_dispatch job for the Playwright smoke suite (#392)