10 KiB
Sprint 1 Security Hotfix Evidence — 2026-08-13
Scope
Implemented and verified the Sprint 1 security packets against isolated temporary data only:
- S1.1 pytest security harness
- S1.2 identity-bound first-time setup
- S1.3 tenant-scoped admin provisioning and updates
- S1.4 request-level setup/user/organization guards
- S1.5 fail-closed bootstrap and JWT configuration
- S1.6 role-aware group/persona response redaction
- Trainee-created persona variants are stored in an owner-private group
- Organization admin PATCH read-modify-write updates use cross-process record locking
- Reviewer follow-up: fail-closed persona allowlist, generic internal errors, upload limits/cleanup, protected super-admin mutations, strict JWT tenant claims, orphan-tenant login denial, and strict boolean terms consent
No production data, .env contents, credentials, deployment settings, commit, or push were touched.
Security behavior verified
| Control | Evidence |
|---|---|
| Setup cannot target another account and the one-time transition is atomic | backend/app/api/auth_routes.py:56-91, backend/app/auth/users.py:181-229, backend/app/storage/store.py:112-173, backend/tests/test_auth_security.py |
Setup requires must_setup and accepted terms |
backend/app/api/auth_routes.py:66-81, backend/tests/test_auth_security.py |
| Cross-tenant provisioning/update denied | backend/app/api/admin_routes.py:97-163, backend/tests/test_admin_tenant_isolation.py |
| Non-super-admin cannot change role/active state | backend/app/api/admin_routes.py:165-179, backend/tests/test_admin_tenant_isolation.py |
| Super-admin group index spans tenants while tenant admins remain scoped | backend/app/api/group_routes.py:182-207, backend/tests/test_admin_tenant_isolation.py |
| Existing tokens re-check user/org state | backend/app/api/helpers.py:44-72, backend/tests/test_request_auth_guards.py |
| Missing/inactive org and token org mismatch denied | backend/app/api/helpers.py:55-67, backend/tests/test_request_auth_guards.py |
| Production bootstrap has no known password fallback | backend/app/config.py:49-81, backend/app/factory.py:13-32, backend/tests/test_bootstrap_config.py |
| Analyze/get/list/persona/variant responses use one policy | backend/app/api/group_routes.py:25-63,239-385, backend/tests/test_group_redaction.py |
| Trainee group view hides uploaded filenames and parsed source text | backend/app/api/group_routes.py:51-70, backend/tests/test_group_redaction.py |
| Trainee variant cannot mutate the shared group or appear to another trainee | backend/app/api/group_routes.py:373-402, backend/app/services/groups.py:74-102, backend/tests/test_group_redaction.py |
Verification results
Executed from repository root unless noted:
backend/.venv/bin/python -m pytest backend/tests -q
77 passed
backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py -q
17 passed
backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py::test_complete_setup_reserves_email_across_concurrent_users -q
1 passed
backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py::test_complete_setup_is_atomic_under_concurrency -q
1 passed
backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py::test_complete_setup_is_atomic_across_worker_processes -q
1 passed
backend/.venv/bin/python -m pytest backend/tests/test_group_redaction.py -q
9 passed
backend/.venv/bin/python -m pytest backend/tests/test_sprint1_review_findings.py -q
24 passed
backend/.venv/bin/python -m pytest backend/tests/test_request_auth_guards.py -q
12 passed
backend/.venv/bin/python -m pytest backend/tests/test_bootstrap_config.py -q
8 passed
backend/.venv/bin/python -m compileall -q backend/app backend/tests backend/scripts
exit 0
backend/scripts/test_*.py
12/12 passed:
test_e2e.py
test_ip_protection.py
test_m0.py
test_m1.py
test_resume_decision.py
test_routes.py
test_saas_tenant.py
test_scenario.py
test_security.py
test_setup.py
test_user_journey.py
test_variant.py
cd frontend && npm run build
vite transformed 1,772 modules; build passed
git diff --check
passed
Additional reviewer-remediation checks:
- Admin persona responses use an explicit allowlist; unknown future persona fields are omitted.
- Group envelopes and tenant-admin input views use explicit allowlists; unknown top-level fields, uploaded filenames, and parsed source payloads are omitted from non-super-admin responses.
- Legacy raw
group.errorvalues are normalized at the API boundary; new failures persist only sanitized error codes and return generic client messages. - Flask
MAX_CONTENT_LENGTHand per-file byte checks enforce upload bounds; partial uploads are removed on save, parse, parser-import, request-data, missing-input, multi-file, and group-persistence failures. - Auth, setup, session-start, persona-update, and profile error paths return fixed public messages; exception logs contain only error types. Active user/org state is literal-boolean fail-closed, and invited-user email uniqueness is enforced by the same locked create path.
- Login responses also expose
must_setuponly when the stored value is literalTrue; no truthiness coercion remains on the reviewed auth path. - Super-admin creation/promotion and role/active mutation paths are blocked through the admin API.
- JWT requests require an exact
org_id; orphaned/inactive organizations and malformed active values cannot log in or pass request guards; placeholder/default bootstrap values are rejected; terms acceptance requires JSON booleantrue. - Trainee private-persona endpoints use the same revealable serializer as shared-group views.
- First-time setup uses a collection lock plus a locked conditional update; threaded and forked-worker
concurrent attempts produce exactly one winner, concurrent setup of different accounts cannot
reserve the same email address, and invite creation cannot race setup for the same email in either
threads or separate worker processes. Service-level setup requires literal
accepted_terms is True, user creation rejects non-booleanmust_setup, and theset_email/setup race has a dedicated regression test. Tenant guards reject missing object tenant IDs instead of defaulting toorg-default.
Explicit limitations
- Docker verification was not run because Docker is not installed in the local environment.
- Real-provider LLM QA was not run; no live provider credential was used.
- Live bootstrap credential/JWT rotation was not performed automatically. Before restoring untrusted/public access, an operator must deploy behind restricted access, set/rotate credentials and
JWT_SECRET, inspect audit data, and run a fresh authenticated smoke test. - Sprint 2 product-correctness defects remain out of scope: session identity/concurrency, persona reply parsing, final-judge debrief, and admin Preview Mode. The private-group lookup still uses the current JSON store's process-local per-file locks; collection-level scan/create atomicity remains an explicit Sprint 2 task.
Current verification — 2026-08-14
The exact current uncommitted tree was re-run after the latest response-boundary, tenant-fallback, session/debrief serializer, and organization-update race fixes:
backend/.venv/bin/python -m pytest backend/tests -q
97 passed in 23.50s
This is the post-remediation exact current-tree result. Earlier background run
(completed before the latest current-tree rerun):
proc_ba80f06ca949 — 91 passed in 928.85s (0:15:28)
Another earlier background run:
proc_efcee6c93382 — 93 passed in 23.11s
The 91- and 93-test results are retained as supporting historical evidence.
The 97-test run above is the authoritative post-remediation exact current-tree
result.
backend/scripts/test_*.py
12/12 passed
cd frontend && npm run build
passed; 1,772 modules transformed
backend/.venv/bin/python -m compileall -q backend/app backend/tests backend/scripts
AST parse + git diff --check
passed
added-line security scan
hardcoded_secret=0
shell_injection=0
eval_exec=0
pickle=0
sql_format=0
Additional current-tree regression coverage includes:
- user/admin listings use a closed user allowlist;
password_hashand unknown internal fields are omitted; - provider-controlled final judge output is constrained to a closed debrief allowlist;
- trainee session/list/resume/get responses omit
user_id,internaljudge state, provider metadata, and unknown message fields; - authenticated group/private-persona mutations reject missing tenant identity instead of falling back to
org-default; - malformed group input/persona shapes serialize safely and admin organization
plan/seatsfields reject coercion; test_org_updates_preserve_concurrent_fields_across_processesverifies that concurrent organizationactiveandseatsupdates do not lose fields across worker processes;JsonStore.update()holds the per-record process lock during read-modify-write.
Independent review
The earlier schema-valid packet/scoped approvals and reviewer timeouts are not approvals of this exact current tree. The exact-tree read-only reviewer (deleg_6e386e71) timed out after 600.19 seconds without schema-valid JSON. The later two-workstream batch (deleg_da73aca4) also timed out in both tasks. The bounded exact-tree reviewer (deleg_fccee30d) returned schema-valid passed=false with one concrete finding: organization admin updates lacked cross-process locking in JsonStore.update(). That finding was fixed and the 97-test run above was executed afterward. The fresh post-remediation exact-tree reviewer (deleg_7bcf0dfd) returned the required schema-valid verdict passed=true, with security_concerns=[] and logic_errors=[]. The Sprint 1 code/reviewer gate is therefore closed. Timeout, partial transcript, delegation status, or local test success must not be treated as approval.
Deployment decision
Do not restore public/untrusted access. The exact-tree code/reviewer gate is closed, but the separate live-operation gate remains pending. Docker verification, real-provider LLM QA, browser/mobile QA, restricted deployment, bootstrap credential change, JWT_SECRET rotation, audit inspection, and authenticated smoke have not been completed. The private-group filesystem scan/create collection race remains explicitly deferred to Sprint 2/S2.1.