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>
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:
adminrole 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_completesetting - 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:
docker logs shopdb-api | grep "Setup token"docker exec shopdb-api flask seed show-setup-tokendocker 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.viewlike other KB reads - Remove
linkurlfrom 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
shopdb/__init__.py- Token logging on first startupshopdb/cli/__init__.py- Newflask seed show-setup-tokencommandshopdb/core/api/setup.py- Token/loopback auth, password minimum, audit logplugins/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 adminunchanged - Existing authenticated workflows unaffected
- Web wizard needs token in URL param (one-line frontend change)