fix: audit de la documentation du 30/09 (8 correctifs de securite, documentation recalee) #199
No reviewers
Labels
No labels
auto-merge
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
AcadeNice/corentin_wakdo!199
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "docs/audit-3009"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Audit independant de la documentation du 30/09 (5 angles : metier, PHP, front, securite, API) et corrections qui en decoulent.
Code (8 defauts, corriges en TDD, deux revues adverses independantes)
POST /api/orders/{n}/pay(public) ne repond que pour une commande de la borne ; comptoir/drive = 404 identique a un numero inconnu.auth.reauth_failed).AccountLockout).role.manage) ; pas de changement de son propre role ; role desactive protege ; listes filtrees.default_routelimite a un chemin local, a la saisie et a la redirection.Referrer-Policy: no-referrersur la page) ;Referrer-Policyde nouveau pose sur les fichiers statiques et erreurs Apache de l'hote admin.Documentation
53 constats corriges (TVA par produit et non par mode de service, 88 renvois au code recontroles, doc API pour la collection Insomnia, doc securite avec limites connues, chiffres finaux, journal du 30/09), contre-audites par deux relecteurs independants.
Verification (code
17c8fe5)PHPStan niveau 6 : 0 erreur. PHPUnit : 2 610 tests, 8 924 assertions, 0 echec, 0 depreciation. JS borne : 492. Navigateur : 212 reussis, 11 sautes, 0 echec ; balayage 4 812, 0 echec. Securite 3 phases : 117 reussis, 0 echec. gitleaks : aucune fuite.
Fusion en commit de fusion (pas de squash) pour que les commits cites dans la documentation restent dans l'historique de
main.POST /api/orders/{number}/pay is anonymous and public, and order numbers are sequential and channel-prefixed (K/C/D + id). OrderRepository::pay() did not filter by source, so anyone could pay a counter/drive order still in pending_payment (it would transition to preparing, go to the kitchen, and decrement stock) or read the status and total of an already-encashed one, even though those orders are entered by an identified staff member, not anonymous. pay() gains an optional restrictToSource parameter, checked before any status read or write; a mismatch throws ORDER_NOT_FOUND, the same response as an unknown number, mirroring show()'s existing anti-enumeration guard. OrderController::pay() (the public route) now restricts to 'kiosk'. createStaffOrder() keeps calling pay() without a restriction, since it has just set the order's source itself (already-trusted channel), so the back-office counter/drive checkout flow is unaffected.GET /reset_password?token=<raw token> is the link sent to reset a password; the combined LogFormat writes the full request line (%r), query string included, so the token landed in the access log until it is consumed or expires (1h). Add a combined_no_query LogFormat that rebuilds the request line from the method, the bare path (%U, no query string) and the HTTP version instead of %r, and route the admin vhost's /reset_password requests to it via SetEnvIf while every other request keeps the regular combined format. The access itself (IP, method, path, status, size) is still logged, only the token is gone. Also corrects the vhost's stale comment claiming X-Forwarded-For is written into the combined log: it isn't (no %{X-Forwarded-For}i field in the format), so %h still logs Traefik's own address, not the real client's; only the PHP layer (Request::clientIp()) reads that header. Verified with `httpd -t` against an image built from this Dockerfile, and end-to-end against a disposable docker-compose stack: before this change, `docker logs <web>` contained the raw token after a GET to /reset_password?token=...; after, the same line is logged without it.Fix 4 FAUX findings (A-1 to A-5): menu slot fields live under data.slots (not detail.slots) and now include option_is_orderable_maxi; document the POST /api/orders/{number}/pay kiosk-only restriction (A-2 code fix already merged); correct the /api/* auth row (no session, not "public or session"); clarify INVALID_ITEM_TYPE/INVALID_IDEMPOTENCY_KEY are kiosk-only, not shared with counter/drive order creation. Document the full POST /api/orders request body (A-4): every field verified against OrderRepository::resolveHeader/resolveAndTotal/resolveLine, a valid example checked against live production catalogue data, and the 9 missing 422 codes plus 404 ORDER_NOT_FOUND added to the error code table. Add the 400 INVALID_JSON row to both error tables in demo-api.md (A-12). Fix the remaining IMPRECIS/COSMETIQUE findings: production status corrected to "en production depuis la release du 29/09 (aab4e96)" everywhere it was stale (A-6); commit citations for the docs/contre-audit branch corrected to point at squash9e22a21/ PR #197 on main instead of a branch name that never existed there (A-7); menu slot reconciliation documented as name-first then position (A-8); DELETE /admin/api/users/{id} 403 on own account documented, plus a new note generalizing the privilege-escalation 403 guard across all /admin/api/users/* routes (A-9); missing role fields listed (A-10); price/VAT PIN wording and CSV import PIN field corrected (A-11); import 409 CONFLICT / 500 IMPORT_FAILED / 403 FORBIDDEN documented, the "plan" response key added to the example, and the Bruno/HTML-only extension check clarified (A-15, A-16); the hypothetical order_item route example marked as such (A-23); PROJECT_CONTEXT.md order-tracking endpoint description corrected to kiosk-only (A-24); demo-api.md's admin email claim reconciled with the intentionally-public demo account (A-25). Not corrected (out of scope, not in the assigned target files): A-13/A-14 (docs/soutenance/oral-blanc-plan-40min.md), A-20 (wiki Comparatif-Back.md), A-21/A-22 (docs/uml/*). No defect found in the Postman/Bruno collections themselves, so they are left untouched.Adversarial review (revue-code.md, D-6 minor, two items): 1. RoleController.php: the default_route refusal message spoke of a "chemin local" and quoted "//", technical vocabulary for a field that is actually a dropdown list ("Page d'accueil après connexion"). Reworded to "Page d'accueil après connexion invalide : choisissez une page de la liste." -- no path/URL jargon, matches the reviewer's suggested wording. 2. RedirectPath.php: the docblock above isLocal() described the colon check as scheme detection ("sans schema ... deja exclu par la regle precedente"), which both misstates what the rule matches (a colon right after the first path segment, e.g. /javascript:alert(1), is not a URL scheme) and, taken literally, suggests the check is redundant. Rewritten to describe the actual bypass pattern it guards against. Test: RoleControllerTest's two default_route refusal assertions now check for the new plain-language phrase instead of the old technical wording.Adversarial review found that closing the raw reset token out of the access log (D-7) was not enough: the vhost's default Referrer-Policy: strict-origin-when-cross-origin still lets a browser attach the full URL, token included, as Referer on every same-origin request the /reset_password page makes -- its own stylesheet, script, logo, and its own form submission -- so the token kept landing in the access log (and in Apache's error log on a proxy failure) via %{Referer}i, valid until consumed or expired (up to 1h). Four Apache-side ways to scope a different value to just this one route were tried and rejected, each verified against a disposable container: a <Location> block never matches this vhost's rewritten request; Header setifempty fails to detect a value the backend already sent and adds a second, ambiguous header; Header ... env= conditions tied to a SetEnvIf or a RewriteRule [E=...] on the request URI are never seen as true by mod_headers even though the identical variable is read correctly by CustomLog on the same request. The admin vhost no longer sets Referrer-Policy at all; App\Core\ Response::headers() (what send() actually emits) now applies "strict-origin-when-cross-origin" as a default whenever a controller hasn't already set the header, so PasswordResetController:: renderConfirm() setting "no-referrer" before send() reliably wins for /reset_password while every other route keeps the prior value. The kiosk vhost, purely static, keeps its own unconditional header. Adds a real-browser Playwright test (security-reset.spec.js) that loads /reset_password, asserts the response header, then loads the page's resources and submits its form while listening for any outgoing Referer -- none is observed.Address findings S-1 to S-15 from the independent security documentation audit (secu.md) and reflect the D-1 to D-7 / A-2 code fixes reviewed twice adversarially (revue-code.md, revue-code-2.md): - SECURITY.md: brute-force line now describes the shared account/profile lockout budget (D-1) and the IP counter no longer reset on success (D-2); Secure cookie described as HTTPS-conditional, not unconditional (S-15); RGPD retention corrected (no order purge exists, S-2); new "Limites connues" section (NAT-shared IP throttle, unverified client IP, SMTP fallback via LogMailer, timing channel). - PROJECT_CONTEXT.md: R3/R8 risk rows updated with the 30/09 fixes and residual limits; PIN reworded "re-authentifie" not "re-autorise" (S-3); classification matrix fixed (password_reset_token_hash is SHA-256, not argon2id, S-6; password_reset_throttle stores a SHA-256 fingerprint, a pseudonymisation not an anonymisation, S-7). - ARCHITECTURE.md, mlt.md, dictionary.md: audit_log described as application-level INSERT-only, not DB-enforced immutability (S-4); PIN re-authentication precision propagated (S-3); RG-9 rewritten to match the D-2 fix; RG-6 and conventions.md corrected: a role change does not close the session, only account deactivation and password reset do (S-13). - domaines/{auth,rbac,users}.md: document the D-1/D-2/D-3 throttle fixes, the D-4 permission-inclusion guard, and the D-6 default_route hardening. - preuves/10-tests-securite.md: production status corrected to "depuis" not "apres" the 29/09 release (S-8); the disabled-role access claim narrowed to match SessionGuard's actual behaviour (S-11); stale file:line citations on files still in motion replaced with function names; new section 8 documents A-2 and D-1 to D-7 with their adversarial-review evidence, including the open D-7.b Referrer-Policy regression on the admin vhost's Apache-served responses. - uml/security-sequence.md, uml/sequence-passer-commande.md: add the missing JSON-body-read step (A-22) and the "id" field in the payment response (A-21). - ADR-0004, ADR-0005: dated errata on audit_log's real immutability guarantee and on the profile-reauth budget that used to wrongly share pin_throttle. - wiki Comparatif-Back.md: the two login throttle counters no longer conflated (S-14). docs/notes/rbac-roles-permissions.md, cited by the audit and listed as a target, does not exist anywhere in this repository's history; the RBAC figures it would have carried already live correctly in docs/demo/matrice-rbac.md.