Enforce consent token on submission, reduce default cooldown
- Require signed _ct cookie (set by POST /api/consent) before accepting any submission. Direct API calls without a prior consent flow are rejected 403. - Add _sign_consent_token / _verify_consent_token (HMAC-SHA256, 24h TTL). - Lower default CONTRIBUTION_COOLDOWN_SECONDS from 3600 to 240 (4 min). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,8 +67,9 @@ RATE_LIMIT_CONTRIBUTIONS = os.environ.get(
|
||||
"RATE_LIMIT_CONTRIBUTIONS", "5 per minute;3 per hour"
|
||||
)
|
||||
CONTRIBUTION_COOLDOWN_SECONDS = int(
|
||||
os.environ.get("CONTRIBUTION_COOLDOWN_SECONDS", "3600")
|
||||
os.environ.get("CONTRIBUTION_COOLDOWN_SECONDS", "240")
|
||||
)
|
||||
CONSENT_TOKEN_TTL = 86400 # 24h — durée de validité du cookie de consentement
|
||||
FLOOD_THRESHOLD = int(os.environ.get("FLOOD_THRESHOLD", "10"))
|
||||
FLOOD_WINDOW_SECONDS = 300
|
||||
|
||||
@@ -230,6 +231,26 @@ def _verify_cooldown(cookie: str, secret: str, cooldown_seconds: int) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _sign_consent_token(secret: str) -> str:
|
||||
ts = int(time.time())
|
||||
msg = f"consent:{ts}".encode()
|
||||
sig = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()[:16]
|
||||
return f"{ts}.{sig}"
|
||||
|
||||
|
||||
def _verify_consent_token(token: str, secret: str) -> bool:
|
||||
try:
|
||||
ts_str, sig = token.rsplit(".", 1)
|
||||
ts = int(ts_str)
|
||||
msg = f"consent:{ts}".encode()
|
||||
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()[:16]
|
||||
if not hmac.compare_digest(sig, expected):
|
||||
return False
|
||||
return (time.time() - ts) < CONSENT_TOKEN_TTL
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _verify_hcaptcha(token: str) -> bool:
|
||||
"""
|
||||
Vérifie un token hCaptcha.
|
||||
@@ -417,6 +438,15 @@ def submit_idea():
|
||||
return jsonify({"error": "captcha_failed", "message": "Vérification CAPTCHA échouée. Veuillez réessayer."}), 400
|
||||
|
||||
secret_key = os.environ.get("SECRET_KEY", "").strip()
|
||||
if secret_key:
|
||||
ct = request.cookies.get("_ct", "")
|
||||
if not ct or not _verify_consent_token(ct, secret_key):
|
||||
logger.info("Soumission refusée — consentement absent ou expiré (IP: %s)", get_remote_address())
|
||||
return jsonify({
|
||||
"error": "consent_required",
|
||||
"message": "Votre consentement est requis avant de contribuer.",
|
||||
}), 403
|
||||
|
||||
if CONTRIBUTION_COOLDOWN_SECONDS > 0 and secret_key:
|
||||
cv = request.cookies.get("_cv", "")
|
||||
if cv and _verify_cooldown(cv, secret_key, CONTRIBUTION_COOLDOWN_SECONDS):
|
||||
@@ -519,7 +549,18 @@ def record_consent():
|
||||
"Consentement enregistré — fingerprint: %s... | version: %s",
|
||||
fingerprint_hash[:8], consent_version,
|
||||
)
|
||||
return jsonify({"ok": True}), 201
|
||||
secret_key = os.environ.get("SECRET_KEY", "").strip()
|
||||
response = make_response(jsonify({"ok": True}), 201)
|
||||
if secret_key:
|
||||
response.set_cookie(
|
||||
"_ct",
|
||||
_sign_consent_token(secret_key),
|
||||
max_age=CONSENT_TOKEN_TTL,
|
||||
httponly=True,
|
||||
samesite="Lax",
|
||||
secure=request.is_secure,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@app.get("/api/stats/public")
|
||||
|
||||
Reference in New Issue
Block a user