Summary
app/database.py hardcodes three demo accounts, including an admin account with a plaintext password, and initialize() unconditionally replaces password_hash for those accounts on every startup (ON CONFLICT(id) DO UPDATE SET ... password_hash = excluded.password_hash). Combined with the published 0.0.0.0:8000 bind, a network-reachable deployment can be logged into with a password that is public in the source tree, and the login endpoint implements no rate limiting or lockout.
Affected behavior
DEMO_USERS in app/database.py contains ("john", ..., "Northstar-John-2026!", "admin") (passwords literal in source).
- On every boot the seed block upserts the demo rows and overwrites
password_hash and role, so even an operator who changed an account's password has it reset at next restart.
compose.yaml runs uvicorn app.main:app --host 0.0.0.0 --port 8000 and publishes 8000:8000.
POST /api/auth/login has no visible rate limiting / account lockout / IP throttle.
- The admin role grants
admin_configuration, which manages the team capability layer: HTTP function tools (with optional stored headers via RELAY_TOOL_HEADER_ENV_ALLOWLIST), remote MCP server configuration, prompt templates and documents.
Verification (local, network-isolated: bound to 127.0.0.1:18099, no real service contacted)
POST /api/auth/login with john@northstar.consulting / Northstar-John-2026! → 200, "role":"admin", capabilities.admin_configuration=true.
POST /api/admin/configuration/function-tools:
- as seeded member
mary → 403 (confirming the endpoint is role-protected; this is a known-default-credential issue, not an unauthenticated one),
- as seeded admin
john with a harmless HTTPS URL → 201 (tool row created; deleted immediately afterwards, no execution).
- Password reseed proof: changed
john's password_hash in the local SQLite file to RESET_PROOF_DISABLED, restarted the service, logged in again with the documented password → 200, admin identity restored.
Full reproduction steps are in the local verification log referenced at the bottom of this report.
Impact
Any deployment reachable over the network is loggable with a source-documented admin credential, and that credential can manage the capability layer (outbound HTTP tools, remote MCP servers, stored secrets). This is a known/mineable credential exposure plus a guaranteed-persistence mechanism (restart restores the password), not an unauthenticated RCE: host code execution was not established, and the project labels the credentials as intentionally demo-only.
Suggested fixes
- Make demo-account seeding opt-in (e.g. only when
RELAY_SEED_DEMO=1) and never overwrite an existing user's password_hash/role.
- Use per-deployment random admin credentials, or require the operator to set them explicitly and fail startup otherwise.
- Add rate limiting / lockout to
/api/auth/login.
- Document deployment warnings for the
0.0.0.0 bind and published port.
References
app/database.py lines ~17-26 (DEMO_USERS), ~400-412 (startup upsert)
app/main.py /api/auth/login, admin configuration routes with require_admin
compose.yaml
Summary
app/database.pyhardcodes three demo accounts, including an admin account with a plaintext password, andinitialize()unconditionally replacespassword_hashfor those accounts on every startup (ON CONFLICT(id) DO UPDATE SET ... password_hash = excluded.password_hash). Combined with the published0.0.0.0:8000bind, a network-reachable deployment can be logged into with a password that is public in the source tree, and the login endpoint implements no rate limiting or lockout.Affected behavior
DEMO_USERSinapp/database.pycontains("john", ..., "Northstar-John-2026!", "admin")(passwords literal in source).password_hashandrole, so even an operator who changed an account's password has it reset at next restart.compose.yamlrunsuvicorn app.main:app --host 0.0.0.0 --port 8000and publishes8000:8000.POST /api/auth/loginhas no visible rate limiting / account lockout / IP throttle.admin_configuration, which manages the team capability layer: HTTP function tools (with optional stored headers viaRELAY_TOOL_HEADER_ENV_ALLOWLIST), remote MCP server configuration, prompt templates and documents.Verification (local, network-isolated: bound to 127.0.0.1:18099, no real service contacted)
POST /api/auth/loginwithjohn@northstar.consulting/Northstar-John-2026!→ 200,"role":"admin",capabilities.admin_configuration=true.POST /api/admin/configuration/function-tools:mary→ 403 (confirming the endpoint is role-protected; this is a known-default-credential issue, not an unauthenticated one),johnwith a harmless HTTPS URL → 201 (tool row created; deleted immediately afterwards, no execution).john'spassword_hashin the local SQLite file toRESET_PROOF_DISABLED, restarted the service, logged in again with the documented password → 200, admin identity restored.Full reproduction steps are in the local verification log referenced at the bottom of this report.
Impact
Any deployment reachable over the network is loggable with a source-documented admin credential, and that credential can manage the capability layer (outbound HTTP tools, remote MCP servers, stored secrets). This is a known/mineable credential exposure plus a guaranteed-persistence mechanism (restart restores the password), not an unauthenticated RCE: host code execution was not established, and the project labels the credentials as intentionally demo-only.
Suggested fixes
RELAY_SEED_DEMO=1) and never overwrite an existing user'spassword_hash/role./api/auth/login.0.0.0.0bind and published port.References
app/database.pylines ~17-26 (DEMO_USERS), ~400-412 (startup upsert)app/main.py/api/auth/login, admin configuration routes withrequire_admincompose.yaml