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:
@@ -15,6 +15,40 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
markdown_bp = Blueprint('markdown', __name__, url_prefix='/api/markdown')
|
||||
|
||||
|
||||
def _permission_of(session, markdown):
|
||||
"""Return the effective permission string ('private'/'protected'/None)."""
|
||||
if markdown.setting_id is None:
|
||||
return None
|
||||
setting = session.query(MarkdownSetting).get(markdown.setting_id)
|
||||
if not setting or not setting.permission_setting_id:
|
||||
return None
|
||||
ps = session.query(MarkdownPermissionSetting).get(setting.permission_setting_id)
|
||||
return ps.permission if ps else None
|
||||
|
||||
|
||||
def _filter_visible(session, markdowns, is_admin):
|
||||
"""
|
||||
Apply markdown permission settings to a list for non-admin callers:
|
||||
private documents are dropped entirely, protected documents have their
|
||||
content masked. Admins see everything. Mirrors get_markdown()'s checks
|
||||
so listing/search endpoints can no longer leak restricted content.
|
||||
"""
|
||||
if is_admin:
|
||||
return [md.to_dict() for md in markdowns]
|
||||
|
||||
visible = []
|
||||
for md in markdowns:
|
||||
permission = _permission_of(session, md)
|
||||
if permission == 'private':
|
||||
continue
|
||||
data = md.to_dict()
|
||||
if permission == 'protected':
|
||||
data['content'] = None
|
||||
data['protected'] = True
|
||||
visible.append(data)
|
||||
return visible
|
||||
|
||||
@markdown_bp.route('/', methods=['GET'])
|
||||
@limiter.limit(api.get_rate_limit)
|
||||
@etag_response
|
||||
@@ -30,9 +64,10 @@ def get_markdowns():
|
||||
Response Codes:
|
||||
- 200: Success
|
||||
"""
|
||||
is_admin = is_user_admin()
|
||||
with get_db() as session:
|
||||
mds = session.query(Markdown).all()
|
||||
return jsonify([md.to_dict() for md in mds]), 200
|
||||
return jsonify(_filter_visible(session, mds, is_admin)), 200
|
||||
|
||||
@markdown_bp.route('/get_home', methods=['GET'])
|
||||
@limiter.limit(api.get_rate_limit)
|
||||
@@ -51,11 +86,15 @@ def get_home():
|
||||
- 200: Success
|
||||
- 204: No content (home markdown not found)
|
||||
"""
|
||||
is_admin = is_user_admin()
|
||||
with get_db() as session:
|
||||
markdown = session.query(Markdown).filter(Markdown.path_id == 1, Markdown.title == "index").first()
|
||||
if markdown is None:
|
||||
return jsonify({}), 204
|
||||
return jsonify(markdown.to_dict()), 200
|
||||
visible = _filter_visible(session, [markdown], is_admin)
|
||||
if not visible:
|
||||
return jsonify({}), 204
|
||||
return jsonify(visible[0]), 200
|
||||
|
||||
@markdown_bp.route('/by_path/<int:path_id>', methods=['GET'])
|
||||
@limiter.limit(api.get_rate_limit)
|
||||
@@ -75,9 +114,10 @@ def get_markdowns_by_path(path_id):
|
||||
Response Codes:
|
||||
- 200: Success
|
||||
"""
|
||||
is_admin = is_user_admin()
|
||||
with get_db() as session:
|
||||
markdowns = session.query(Markdown).filter(Markdown.path_id == path_id).all()
|
||||
return jsonify([md.to_dict() for md in markdowns]), 200
|
||||
return jsonify(_filter_visible(session, markdowns, is_admin)), 200
|
||||
|
||||
@markdown_bp.route('/get_index/<int:path_id>', methods=['GET'])
|
||||
@limiter.limit(api.get_rate_limit)
|
||||
@@ -99,11 +139,15 @@ def get_index(path_id):
|
||||
- 200: Success
|
||||
- 204: No content (index markdown not found)
|
||||
"""
|
||||
is_admin = is_user_admin()
|
||||
with get_db() as session:
|
||||
markdown = session.query(Markdown).filter(Markdown.path_id == path_id).filter(Markdown.title == "index").first()
|
||||
if markdown is None:
|
||||
return jsonify({}), 204
|
||||
return jsonify(markdown.to_dict()), 200
|
||||
visible = _filter_visible(session, [markdown], is_admin)
|
||||
if not visible:
|
||||
return jsonify({}), 204
|
||||
return jsonify(visible[0]), 200
|
||||
|
||||
|
||||
|
||||
@@ -429,11 +473,19 @@ def search_markdowns(keyword):
|
||||
Response Codes:
|
||||
- 200: Success
|
||||
"""
|
||||
is_admin = is_user_admin()
|
||||
# Escape LIKE metacharacters so user input can't inject wildcards
|
||||
# (full-table-scan DoS) and so search actually matches substrings.
|
||||
escaped = keyword.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
|
||||
pattern = f"%{escaped}%"
|
||||
with get_db() as session:
|
||||
res = session.query(Markdown).filter(
|
||||
or_(Markdown.title.like(keyword), Markdown.content.like(keyword))
|
||||
).all()
|
||||
return jsonify([md.to_dict() for md in res]), 200
|
||||
or_(
|
||||
Markdown.title.like(pattern, escape='\\'),
|
||||
Markdown.content.like(pattern, escape='\\'),
|
||||
)
|
||||
).limit(200).all()
|
||||
return jsonify(_filter_visible(session, res, is_admin)), 200
|
||||
|
||||
@markdown_bp.route('/links', methods=['GET'])
|
||||
@limiter.limit(api.get_rate_limit)
|
||||
|
||||
Reference in New Issue
Block a user