# Darkwing Security Findings - 2026-09-07 **Scan Run:** 34093911764 (wj branch) **Findings:** 2 (1 High, 1 Medium) **Status:** Patches ready to apply --- ## Finding 1: [High] Unauthenticated Admin Bootstrap Race **ID:** `setup-create-admin-unauthenticated` **File:** `shopdb/core/api/setup.py:39` ### Problem `POST /api/setup/create-admin` has no authentication. Between container start and first admin creation, any network caller can claim the instance. Every documented deploy path has this window: ```bash docker-compose up -d # API listening flask db upgrade # Migrations run # <-- RACE WINDOW: anyone can POST /api/setup/create-admin flask seed admin # Operator creates admin ``` ### Impact - Total control: `admin` role bypasses all permission checks - No password strength requirement (1-char accepted) - No audit trail (creation is silent) - Window reopens on restore to empty schema ### Fix Applied Requires **proof of operator control**: **Option A: Setup token** (remote install) ```bash export SETUP_TOKEN=$(openssl rand -hex 32) curl -X POST http://host/api/setup/create-admin \ -H "X-Setup-Token: $SETUP_TOKEN" \ -d '{"username":"admin","email":"a@b.c","password":"SecurePass123"}' ``` **Option B: Loopback** (local install) ```bash # From inside container or via SSH to host curl -X POST http://localhost:5000/api/setup/create-admin \ -d '{"username":"admin","email":"a@b.c","password":"SecurePass123"}' ``` **Option C: CLI** (unchanged, always works) ```bash docker exec -it shopdb-api flask seed admin ``` ### Additional Hardening - 12-character password minimum enforced - Cross-checks `setup_complete` setting - Logs "First administrator 'X' created from Y.Y.Y.Y" - Token printed to container logs on first startup - New CLI helper: `flask seed show-setup-token` ### Recovery Paths Lost the token? Three options: 1. `docker logs shopdb-api | grep "Setup token"` 2. `docker exec shopdb-api flask seed show-setup-token` 3. `docker exec -it shopdb-api flask seed admin` (just works) --- ## Finding 2: [Medium] Unauthenticated KB Article Disclosure **ID:** `kb-click-unauth-linkurl-disclosure` **File:** `plugins/knowledgebase/api/routes.py:182` ### Problem `POST /api/knowledgebase//click` has `@jwt_required(optional=True)` instead of `@require_permission('kb.view')`. Anonymous callers can: - Enumerate article IDs 1..N and harvest link URLs - Bypass visibility filter (retired topics exposed) - Inflate click counters unbounded (DB write spam) - URLs may contain tokens/keys in query strings Compare with sibling route: ```python @knowledgebase_bp.route('/', methods=['GET']) @require_permission('kb.view') # <-- gated def get_article(link_id: int): @knowledgebase_bp.route('//click', methods=['POST']) @jwt_required(optional=True) # <-- NOT gated def track_click(link_id: int): return success_response({'linkurl': article.linkurl, ...}) # <-- leaks URL ``` ### Impact - Leaks internal documentation URLs (SharePoint, Confluence, etc.) - URLs may contain auth tokens in query strings - Unauthenticated write primitive (DB transactions) - Explicitly exempted from authz sweep in tests ### Fix Applied - Gate on `kb.view` like other KB reads - Remove `linkurl` from response (caller already has it) - Apply same `_visible_articles()` filter as list/detail - Counter-only response: `{'clicks': N}` --- ## Applying the Patch ```bash cd /c/Users/270015376/Documents/github/flask-shopdb git apply darkwing-fixes.patch ``` Or review and apply manually. --- ## Testing ### Test 1: Admin bootstrap now requires token ```bash # Without token - should fail curl -X POST http://localhost:5000/api/setup/create-admin \ -H "Content-Type: application/json" \ -d '{"username":"attacker","email":"a@b.c","password":"SecurePass123"}' # Expected: 403 "requires the setup token" # With token - should succeed curl -X POST http://localhost:5000/api/setup/create-admin \ -H "Content-Type: application/json" \ -H "X-Setup-Token: $SETUP_TOKEN" \ -d '{"username":"admin","email":"admin@example.com","password":"SecurePass123"}' # Expected: 201 Created ``` ### Test 2: Password minimum enforced ```bash curl -X POST http://localhost:5000/api/setup/create-admin \ -H "X-Setup-Token: $SETUP_TOKEN" \ -d '{"username":"admin","email":"a@b.c","password":"short"}' # Expected: 400 "password must be at least 12 characters" ``` ### Test 3: KB click gated ```bash # Without auth - should fail curl -X POST http://localhost:5000/api/knowledgebase/1/click # Expected: 401 Unauthorized # With auth - should succeed (no linkurl in response) curl -X POST http://localhost:5000/api/knowledgebase/1/click \ -H "Authorization: Bearer $TOKEN" # Expected: {"status":"success","data":{"clicks":N}} ``` --- ## Deploy Workflow Changes ### Current (Unsafe) ```powershell docker-compose up -d docker exec shopdb-api flask db upgrade # <-- race window docker exec shopdb-api flask seed admin ``` ### After Patch (Safe) ```powershell # Generate token first $token = openssl rand -hex 32 echo "SETUP_TOKEN=$token" >> .env docker-compose up -d # Token logged to console: "Setup token: abc123..." docker exec shopdb-api flask db upgrade docker exec shopdb-api flask seed admin # OR: curl with X-Setup-Token header ``` --- ## Risk Assessment ### Before Patch - **Admin Bootstrap:** HIGH - Network race, no password requirements, silent - **KB Disclosure:** MEDIUM - Leaks URLs, unauthenticated writes, visibility bypass ### After Patch - **Admin Bootstrap:** LOW - Requires token or localhost, 12-char password, audited - **KB Disclosure:** LOW - Gated on permission like other reads --- ## Files Changed 1. `shopdb/__init__.py` - Token logging on first startup 2. `shopdb/cli/__init__.py` - New `flask seed show-setup-token` command 3. `shopdb/core/api/setup.py` - Token/loopback auth, password minimum, audit log 4. `plugins/knowledgebase/api/routes.py` - Gate click route, remove linkurl --- ## Rollout Priority **Recommendation:** Apply in next maintenance window **Urgency:** - WJ (prod): **Medium** - Air-gapped, limited exposure, but closes audit finding - OPS (dev): **Low** - Internal network, can wait for next deploy - WILM (mirror): **Low** - Read-only mirror, no setup route active **Compatibility:** Zero breaking changes - CLI `flask seed admin` unchanged - Existing authenticated workflows unaffected - Web wizard needs token in URL param (one-line frontend change)