`core.middleware.require_admin` grants admin to any request whose `request.state.current_user == "internal-tool"` — the sentinel meant only for the in-process tool-loopback path. But the normal cookie auth path (app.py) sets `current_user` to the raw username, and neither `create_user` nor the signup route reserved that name. As a result an account literally named "internal-tool" was silently treated as admin by every `require_admin`-gated route. With self-service signup enabled this is an anonymous -> admin privilege escalation. Reserve the full synthetic-owner set the codebase already special-cases — "internal-tool", "api", "demo", "system" (see `_SYNTHETIC_OWNERS` in routes/assistant_routes.py and the matching guards in src/task_scheduler.py and routes/research_routes.py). "api" collides with the bearer-token owner sentinel; "demo"/"system" would leave a real account denied an assistant and inconsistently owner-scoped. Refuse to create or rename into any reserved name (case/space-normalized), and reject empty usernames while we're here. Adds a regression test. Co-authored-by: Claude <noreply@anthropic.com>
2.5 KiB
2.5 KiB