Security hardening: fix RCE, auth and SSRF issues
Critical: - backup: prevent Zip Slip path traversal and zip bombs in restore/convert via safe_extract(); serialize get_backup() with backup_lock and always restore CWD so concurrent requests can't corrupt the os.chdir state - app: only enable the Werkzeug debugger/reloader when ENVIRONMENT=dev; always init rate limits (also under WSGI), not just under __main__ - apikey: fix create_key never committing (session.commit -> commit()), validate roles against an allowlist, and fix revoke_key/update_last_used operating on detached instances so revocation actually persists - env_provider: redact DB_PASSWORD and SESSION_SECRET_KEY in summerize() High: - markdown: filter private/protected docs for non-admins in the listing, get_home, get_index and search endpoints (was an anonymous data leak); escape LIKE metacharacters and cap search results - webhooks: validate target URL to block SSRF (loopback/private/link-local/ metadata IPs), disable redirects, safely parse additional_header - auth: validate JWT issuer and require exp/iat; add timeout to JWKS fetch; harden Authorization header parsing against malformed values - log: require admin for GET /api/log and auth for POST; bound entry size Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +1,46 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from api import get_api_key, generate_api_key
|
||||
from api import generate_api_key
|
||||
from db import get_db
|
||||
from api import require_auth
|
||||
from db.models.APIKey import APIKey
|
||||
|
||||
api_key_bp = Blueprint('apikey', __name__, url_prefix='/api/apikey')
|
||||
|
||||
# An API key must never be able to request a role broader than what the
|
||||
# product defines, regardless of what the request body asks for.
|
||||
ALLOWED_API_KEY_ROLES = {'admin', 'creator', 'user'}
|
||||
|
||||
@api_key_bp.route('/', methods=['POST'])
|
||||
@require_auth(roles=['admin'])
|
||||
def create_key():
|
||||
data = request.get_json()
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
|
||||
if not data or 'name' not in data:
|
||||
return jsonify({"error": "Name is required"}), 400
|
||||
|
||||
roles = data.get('roles', [])
|
||||
if not isinstance(roles, list) or any(r not in ALLOWED_API_KEY_ROLES for r in roles):
|
||||
return jsonify({"error": f"roles must be a subset of {sorted(ALLOWED_API_KEY_ROLES)}"}), 400
|
||||
|
||||
try:
|
||||
with get_db() as session:
|
||||
apikey = APIKey(key=generate_api_key(),name=data['name'], roles=roles)
|
||||
session.add(apikey);
|
||||
session.commit
|
||||
return jsonify(apikey.to_dict()), 201
|
||||
apikey = APIKey(key=generate_api_key(), name=data['name'], roles=roles)
|
||||
session.add(apikey)
|
||||
session.commit()
|
||||
result = apikey.to_dict()
|
||||
return jsonify(result), 201
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@api_key_bp.route('/<key>', methods=['DELETE'])
|
||||
@require_auth(roles=['admin'])
|
||||
def revoke_key(key):
|
||||
|
||||
api_key = get_api_key(key)
|
||||
# Query and mutate within the same session, otherwise the update is
|
||||
# performed on a detached instance and silently never persists.
|
||||
with get_db() as session:
|
||||
api_key = session.query(APIKey).filter_by(key=key, is_active=True).first()
|
||||
if not api_key:
|
||||
return jsonify({"error": "API key not found"}), 404
|
||||
api_key.is_active = False
|
||||
session.commit()
|
||||
return jsonify({"message": "API key revoked successfully"}), 200
|
||||
return jsonify({"message": "API key revoked successfully"}), 200
|
||||
|
||||
Reference in New Issue
Block a user