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:
@@ -1,7 +1,6 @@
|
||||
"""Knowledge Base API endpoints."""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.api import (
|
||||
intarg,
|
||||
@@ -180,21 +179,29 @@ def get_article(link_id: int):
|
||||
|
||||
|
||||
@knowledgebase_bp.route('/<int:link_id>/click', methods=['POST'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('kb.view')
|
||||
def track_click(link_id: int):
|
||||
"""Increment click counter and return the URL to redirect to."""
|
||||
article = db.session.get(KnowledgeBase, link_id)
|
||||
"""Increment the click counter for an article the caller is allowed to read.
|
||||
|
||||
if not article or not article.isactive:
|
||||
Gated on kb.view like every other read of this table: an anonymous caller
|
||||
walking link ids must not be able to probe which articles exist, drive the
|
||||
counters that order the list, or spend a DB transaction per request.
|
||||
|
||||
Restricted to the same visible set as the list/detail reads so an article
|
||||
about a retired application is not reachable here either.
|
||||
|
||||
The linkurl is deliberately not echoed back: whoever is permitted to click
|
||||
already received it in the list or detail response the click came from.
|
||||
"""
|
||||
article = _visible_articles().filter(KnowledgeBase.linkid == link_id).first()
|
||||
|
||||
if not article:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404)
|
||||
|
||||
article.increment_clicks()
|
||||
db.session.commit()
|
||||
|
||||
return success_response({
|
||||
'linkurl': article.linkurl,
|
||||
'clicks': article.clicks
|
||||
})
|
||||
return success_response({'clicks': article.clicks})
|
||||
|
||||
|
||||
@knowledgebase_bp.route('', methods=['POST'])
|
||||
|
||||
Reference in New Issue
Block a user