- users module: profile + address CRUD behind use cases (users_profiles, users_addresses) - roles customer/admin on identity_users; role resolved from DB per request - shared auth contract (Authenticate, requireRole, requireOwnerOrAdmin) injected from composition root; users never imports identity - authorization runs before existence checks; address SQL scoped by user_id - @fastify/cookie registered once at app root (cross-module) - migrations 003_identity_roles + 004_users (reversible) - no new npm dependencies; tests: unit 52, integration 22 Gates: reviewer/security/qa APPROVED; verify.sh green
41 lines
2.2 KiB
JSON
41 lines
2.2 KiB
JSON
{
|
|
"feature_id": "F-006",
|
|
"agent": "security",
|
|
"verdict": "APPROVED",
|
|
"threat_model": [
|
|
{
|
|
"vector": "SQL injection",
|
|
"result": "mitigated",
|
|
"evidence": "Every query parameterized ($n). The only dynamic SQL (profile upsert, address update) builds SET clauses from a hardcoded column whitelist; user input only flows into parameter values."
|
|
},
|
|
{
|
|
"vector": "Privilege escalation (client sets own role)",
|
|
"result": "mitigated",
|
|
"evidence": "register/login schemas accept only email+password; no route writes identity_users.role. Role is returned from DB values, never echoed from input. DB CHECK constraint limits role to customer|admin."
|
|
},
|
|
{
|
|
"vector": "Broken access control / IDOR",
|
|
"result": "mitigated",
|
|
"evidence": "Each users route runs authenticate -> requireOwnerOrAdmin before data access; address repository scopes every query by user_id, so a valid foreign addressId is unreachable. Integration tests prove cross-user reads and mutations all return 403."
|
|
},
|
|
{
|
|
"vector": "Resource existence enumeration",
|
|
"result": "mitigated",
|
|
"evidence": "Authorization (403) is evaluated before existence checks (404) for non-owners, so a stranger cannot learn whether another user's profile exists."
|
|
},
|
|
{
|
|
"vector": "Stale authorization (demotion/promotion)",
|
|
"result": "mitigated",
|
|
"evidence": "Session authenticator resolves role from identity_users on every request; role changes apply immediately without re-login, and no role state is trusted from the client."
|
|
},
|
|
{
|
|
"vector": "Session security",
|
|
"result": "unchanged from F-005",
|
|
"evidence": "Opaque token in HttpOnly/SameSite=Lax cookie, SHA-256 hash stored server-side, expiry + revocation enforced in SQL. Cookie plugin moved to app root (cross-module), same policy."
|
|
}
|
|
],
|
|
"dependencies_added": [],
|
|
"notes": "No new npm dependencies; spec/tech.md unchanged. The 403-vs-404 ordering is correct for anti-enumeration. GET /users admin list returning only profiled users is not a security issue (admin-only surface).",
|
|
"evidence": "npm run test:integration: 22 passed including forged-cookie 401, cross-user 403 battery, and logout revocation regression"
|
|
}
|