security: fix admin bootstrap race and KB article disclosure

Address two security findings from darkwing scan:

Finding 1 [High]: Unauthenticated admin bootstrap race
- Require SETUP_TOKEN header or loopback origin for POST /api/setup/create-admin
- Enforce 12-character minimum password for bootstrap admin
- Cross-check setup_complete setting to prevent re-arming
- Log token to container logs on first startup for operator retrieval
- Add flask seed show-setup-token CLI helper
- Add audit log entry when first admin is created
- Preserve existing flask seed admin workflow (uses loopback)

Finding 2 [Medium]: Unauthenticated KB article disclosure
- Gate POST /api/knowledgebase/<id>/click on kb.view permission
- Remove linkurl from response (caller already has it from GET)
- Apply _visible_articles() filter to respect retired applications
- Prevent anonymous link enumeration and counter manipulation

Recovery paths if token is lost:
1. docker logs shopdb-api | grep "Setup token"
2. docker exec shopdb-api flask seed show-setup-token
3. docker exec shopdb-api flask seed admin (unchanged, always works)

Zero breaking changes. CLI and existing workflows unaffected.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Proudlock
2026-09-07 03:37:22 -04:00
parent 75fe3dd3f0
commit 397c02611a
4 changed files with 122 additions and 12 deletions

View File

@@ -199,6 +199,32 @@ def seed_cli():
pass
@seed_cli.command('show-setup-token')
@with_appcontext
def show_setup_token():
"""Display the setup token for first-admin creation.
The token gates POST /api/setup/create-admin so a network race cannot claim
the instance during the install window. Printed at first startup; retrieve
it here if the logs rotated or you lost the terminal.
"""
import os
from flask import current_app
from shopdb.core.models import User
if User.query.count() > 0:
click.echo(click.style("Setup already complete (users exist).", fg='green'))
return
token = current_app.config.get('SETUP_TOKEN') or os.environ.get('SETUP_TOKEN')
if token:
click.echo(click.style("Setup token: ", fg='cyan') + token)
click.echo("Use: curl -X POST .../api/setup/create-admin -H 'X-Setup-Token: <token>' ...")
else:
click.echo(click.style("No SETUP_TOKEN configured.", fg='yellow'))
click.echo("Use 'flask seed admin' from inside the container (no token needed).")
@seed_cli.command('catalog')
@click.option('--file', 'path', default=None,
help='catalog JSON to load (default shopdb/data/catalog.json)')