feat(6838): read the client address and HTTPS from the trusted proxy hop
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Failing after 1m20s
CI & Build / integration (push) Successful in 2m27s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Failing after 1m20s
CI & Build / integration (push) Successful in 2m27s
CI & Build / Build & push image (push) Skipped
grimoire/proxy.py is the one reader of X-Forwarded-For and X-Forwarded-Proto. It takes the entry TRUSTED_PROXY_HOPS in from the right (default 1; a shorter header reads its leftmost entry; 0 trusts no proxy), so an entry a client puts left of the proxy's is never believed. Before this, with TRUST_PROXY_HEADERS on, the leftmost X-Forwarded-For entry keyed the login, register, forgot and reset rate limits and the audit log, and a client could choose it. The session cookie is Secure, and HSTS (max-age=31536000) is sent, exactly when the request is HTTPS by the trusted proxy's word. Plain HTTP is untouched and nothing redirects. SECURE_COOKIES is gone (a startup warning says so when it is still set); TRUST_PROXY_HEADERS=false still maps to 0 hops, with a warning to move to TRUSTED_PROXY_HOPS. Admin audit lines now use the same client address instead of the socket peer. The placeholder-key refusal keyed on SECURE_COOKIES, so it now refuses either placeholder key outright; the instance makes its own key when none is set. Family idea #5004 (IsHTTPS, trusted-hop X-Forwarded-Proto), canon v2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import json
|
||||
from quart import Blueprint, Response, g, jsonify, request
|
||||
|
||||
from grimoire.auth import admin_required, login_required, get_current_user_id
|
||||
from grimoire.proxy import client_ip
|
||||
from grimoire.services.auth import (
|
||||
create_invitation,
|
||||
delete_user,
|
||||
@@ -46,7 +47,7 @@ async def backup():
|
||||
else:
|
||||
data = await export_user_backup(uid)
|
||||
|
||||
await log_audit("backup", user_id=uid, username=g.user.username, ip_address=request.remote_addr, details={"scope": scope})
|
||||
await log_audit("backup", user_id=uid, username=g.user.username, ip_address=client_ip(), details={"scope": scope})
|
||||
return Response(
|
||||
json.dumps(data, indent=2, default=str),
|
||||
content_type="application/json",
|
||||
@@ -79,7 +80,7 @@ async def restore():
|
||||
|
||||
stats = await restore_full_backup(data)
|
||||
uid = get_current_user_id()
|
||||
await log_audit("restore", user_id=uid, username=g.user.username, ip_address=request.remote_addr, details={"stats": stats})
|
||||
await log_audit("restore", user_id=uid, username=g.user.username, ip_address=client_ip(), details={"stats": stats})
|
||||
return jsonify({"status": "ok", "stats": stats})
|
||||
|
||||
|
||||
@@ -100,7 +101,7 @@ async def remove_user(user_id: int):
|
||||
deleted = await delete_user(user_id)
|
||||
if not deleted:
|
||||
return jsonify({"error": "User not found"}), 404
|
||||
await log_audit("user_delete", user_id=current_uid, username=g.user.username, ip_address=request.remote_addr, details={"deleted_user_id": user_id})
|
||||
await log_audit("user_delete", user_id=current_uid, username=g.user.username, ip_address=client_ip(), details={"deleted_user_id": user_id})
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@@ -121,7 +122,7 @@ async def toggle_registration():
|
||||
|
||||
uid = get_current_user_id()
|
||||
await set_registration_open(uid, bool(open_val))
|
||||
await log_audit("registration_toggle", user_id=uid, username=g.user.username, ip_address=request.remote_addr, details={"open": bool(open_val)})
|
||||
await log_audit("registration_toggle", user_id=uid, username=g.user.username, ip_address=client_ip(), details={"open": bool(open_val)})
|
||||
return jsonify({"status": "ok", "open": bool(open_val)})
|
||||
|
||||
|
||||
@@ -151,7 +152,7 @@ async def update_smtp():
|
||||
|
||||
if settings_to_save:
|
||||
await set_settings_batch(uid, settings_to_save)
|
||||
await log_audit("smtp_config", user_id=uid, username=g.user.username, ip_address=request.remote_addr)
|
||||
await log_audit("smtp_config", user_id=uid, username=g.user.username, ip_address=client_ip())
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@@ -166,7 +167,7 @@ async def test_smtp():
|
||||
uid = get_current_user_id()
|
||||
try:
|
||||
await send_test_email(recipient)
|
||||
await log_audit("smtp_test", user_id=uid, username=g.user.username, ip_address=request.remote_addr, details={"recipient": recipient})
|
||||
await log_audit("smtp_test", user_id=uid, username=g.user.username, ip_address=client_ip(), details={"recipient": recipient})
|
||||
return jsonify({"status": "ok"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -209,7 +210,7 @@ async def update_forge_webhook_settings():
|
||||
# The secret is deliberately absent from the audit detail.
|
||||
await log_audit(
|
||||
"forge_webhook_config", user_id=uid, username=g.user.username,
|
||||
ip_address=request.remote_addr, details={},
|
||||
ip_address=client_ip(), details={},
|
||||
)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@@ -262,7 +263,7 @@ async def update_base_url():
|
||||
return jsonify({"error": "Base URL must use http or https"}), 400
|
||||
uid = get_current_user_id()
|
||||
await set_setting(uid, "base_url", url)
|
||||
await log_audit("base_url_config", user_id=uid, username=g.user.username, ip_address=request.remote_addr, details={"base_url": url})
|
||||
await log_audit("base_url_config", user_id=uid, username=g.user.username, ip_address=client_ip(), details={"base_url": url})
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@@ -310,7 +311,7 @@ async def update_db_maintenance():
|
||||
uid = get_current_user_id()
|
||||
await log_audit(
|
||||
"db_maintenance_config", user_id=uid, username=g.user.username,
|
||||
ip_address=request.remote_addr, details={"enabled": enabled, "hour": hour},
|
||||
ip_address=client_ip(), details={"enabled": enabled, "hour": hour},
|
||||
)
|
||||
return jsonify({"status": "ok", "enabled": enabled, "hour": hour})
|
||||
|
||||
@@ -323,7 +324,7 @@ async def run_db_maintenance_now():
|
||||
uid = get_current_user_id()
|
||||
await log_audit(
|
||||
"db_maintenance_run", user_id=uid, username=g.user.username,
|
||||
ip_address=request.remote_addr,
|
||||
ip_address=client_ip(),
|
||||
)
|
||||
summary = await run_maintenance()
|
||||
return jsonify(summary)
|
||||
@@ -389,7 +390,7 @@ async def update_backups():
|
||||
uid = get_current_user_id()
|
||||
await log_audit(
|
||||
"backup_config", user_id=uid, username=g.user.username,
|
||||
ip_address=request.remote_addr,
|
||||
ip_address=client_ip(),
|
||||
details={**config.to_dict(), "secret_changed": bool(
|
||||
data.get("clear_s3_secret") or (isinstance(secret, str) and secret and secret != SECRET_MASK)
|
||||
)},
|
||||
@@ -439,7 +440,7 @@ async def run_backup_now():
|
||||
return jsonify({"error": "A backup is already running."}), 409
|
||||
uid = get_current_user_id()
|
||||
await log_audit("backup_run", user_id=uid, username=g.user.username,
|
||||
ip_address=request.remote_addr, details={})
|
||||
ip_address=client_ip(), details={})
|
||||
return jsonify({"status": "started"}), 202
|
||||
|
||||
|
||||
@@ -463,7 +464,7 @@ async def create_invite():
|
||||
"invitation_created",
|
||||
user_id=uid,
|
||||
username=g.user.username,
|
||||
ip_address=request.remote_addr,
|
||||
ip_address=client_ip(),
|
||||
details={"invited_email": email},
|
||||
)
|
||||
return jsonify({"status": "ok"}), 201
|
||||
@@ -497,7 +498,7 @@ async def delete_invitation(invitation_id: int):
|
||||
"invitation_revoked",
|
||||
user_id=uid,
|
||||
username=g.user.username,
|
||||
ip_address=request.remote_addr,
|
||||
ip_address=client_ip(),
|
||||
details={"invitation_id": invitation_id},
|
||||
)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
Reference in New Issue
Block a user