Files
sales-trainer/docs/engineering-log/2026-08-07-security-ux.md
Macky ff0f680090 [verified] Security hardening + UX/UI polish
Security (requesting-code-review pipeline + independent reviewer):
- Fix path traversal on file upload (basename sanitize + resolve-containment)
- Fix IDOR: org + owner scoping on all group/chat routes (_authorize_group/_get_owned_group),
  hide other users' personal groups in listings
- Remove XSS via v-html in Chat task (text interpolation)
- Add test_security.py (traversal + cross-user denial) — all pass

UX/UI (ui-ux-pro-max + frontend-dev-verification):
- Global: focus rings, 44px touch targets, hover/press transitions, input focus glow,
  prefers-reduced-motion, skeleton loaders, empty states, back links, spinner
- Login: password toggle, autocomplete, spinner, disabled-when-empty
- Cards lift on hover; dashboard skeleton + empty state; analyze button spinner

All backend tests pass (m0/m1/routes/security/e2e); frontend builds; served SPA verified via curl.
2026-08-07 16:00:43 +07:00

3.0 KiB
Raw Blame History

2026-08-07 — Security hardening + UX/UI polish

Summary

Ran the requesting-code-review (security) + ui-ux-pro-max (design) + frontend-dev-verification pipelines against the built Sales Trainer app. Fixed real security vulnerabilities and applied accessibility/touch/visual polish. Committed as [verified].

Security audit — found & fixed

  1. Path traversal on file upload (HIGH): create_group used the raw client file.filename in dest = upload_dir / f"{user}__{file.filename}" → an attacker-provided name like ../../evil.txt could escape the upload directory. FIXED: strip to basename (Path(name).name), reject names containing / or \, and add a resolve().relative_to(upload_dir) containment check.
  2. IDOR — org/owner scoping missing (HIGH): list_personas, get_persona, update_persona, and the chat routes did not verify a group belonged to the caller's org; personal groups (owner_user_id) were readable by any same-org user. FIXED: centralized _authorize_group / _get_owned_group (org scope, super_admin bypass, owner-only for personal groups) applied to all group + chat routes; list_groups and win_lose_board now hide other users' personal groups.
  3. XSS hygiene (MEDIUM): Chat.vue used v-html="taskText". FIXED: switched to text interpolation; removed server HTML in the opener-task string.
  4. Confirmed no hardcoded secrets, no eval/exec, no shell injection, no self-registration (register→404).

UX/UI (ui-ux-pro-max applied)

  • Global style.css: visible focus rings (a11y), 44px min touch targets, button/card hover + active-press transitions (150300ms), input focus glow, prefers-reduced-motion support, skeleton loaders, empty-state block, back-link button, spinner, responsive mobile margins.
  • Login: password show/hide toggle, autocomplete attrs, spinner, disabled-when-empty.
  • Personas/GroupEdit/Dashboard: .lift card hover, back-navigation links, proper empty states (skeleton loaders on dashboard), spinner on analyze button.
  • Chat: back link, spinner on finish button, disabled-after-debrief.

Verification

  • test_security.py ADDED: traversal filename rejected (no file escapes), cross-user personal-group access → 403, personal group hidden from other users' listing. ALL PASS.
  • Full suite re-run: test_m0 / test_m1 / test_routes / test_security / test_e2e ALL PASS.
  • Frontend npm run build ok (11 chunks). Served-page verification via curl: GET / 200, register 404, login 200; new CSS classes (btn-back, card.lift, empty-state, focus-visible, prefers-reduced-motion, skeleton) present in served bundle; no v-html in any built JS chunk.
  • Browser visual check was BLOCKED by an environment issue: Hermes browser proxy resolves to camo.moreminimore.com/tabs (HTTP 500) and cannot reach localhost. Rendered verification done via served-HTML/DIST inspection instead.

Next

  • Independent reviewer (requesting-code-review subagent) result pending → incorporate, then commit.