Files
shopdb-flask/DARKWING-FINDINGS.md
Proudlock 86256e7041 Add Darkwing security scan results and remediation artifacts
Security scan completed on 2026-09-08, documenting findings and proposed fixes for attack surface review.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-09-08 11:35:40 -04:00

6.3 KiB

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:

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)

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)

# 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)

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/<id>/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:

@knowledgebase_bp.route('/<int:link_id>', methods=['GET'])
@require_permission('kb.view')  # <-- gated
def get_article(link_id: int):

@knowledgebase_bp.route('/<int:link_id>/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

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

# 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

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

# 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)

docker-compose up -d
docker exec shopdb-api flask db upgrade
# <-- race window
docker exec shopdb-api flask seed admin

After Patch (Safe)

# 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)