security: hardening contra 7 vectores de ataque identificados
Análisis ofensivo → defensas implementadas:
1. /health/history sin auth (CRÍTICO)
- Requiere Bearer token (HEALTH_AUTH_TOKEN en .env)
- chat_ids enmascarados como hash SHA256[:8] opaco ("usr_a3b4c5d6")
- Accesos denegados loggeados con IP + User-Agent
2. /health revela inteligencia operacional (MEDIO)
- Sin auth: solo {"status", "timestamp"} — sin conteo de tenants ni timing
- Con auth: respuesta completa con métricas internas
- Misma URL, auth desbloquea detalle
3. Variables sensibles en proceso env (CRÍTICO)
- Después de cargar Settings(), se limpian FERNET_KEY, TELEGRAM_BOT_TOKEN,
PJ_USUARIO_ENC, PJ_PASSWORD_ENC, WEBHOOK_SECRET, HEALTH_AUTH_TOKEN, INVITE_CODE
- Protege contra /proc/<pid>/environ y herencia por subprocesos
4. Race condition TOCTOU en invite codes (MEDIO)
- SELECT+UPDATE reemplazado por UPDATE WHERE usado=0
- rowcount=0 → False; imposible que dos requests simultáneos consuman el mismo código
5. Timing oracle en invite codes (BAJO-MEDIO)
- Delay mínimo de 800ms en _paso_codigo() independientemente del resultado
- secrets.compare_digest() para comparación del código del .env
6. Information disclosure en respuestas a no-registrados (BAJO)
- _rechazar_no_registrado: "Para usar este bot escribí /start." (sin revelar comandos)
- handle_texto_libre: silencio total para usuarios sin registro (no confirmar que el bot existe)
7. Nginx rate limiting (DEPLOY.md)
- limit_req_zone pedrito_webhook: 30r/s, burst=50
- limit_req_zone pedrito_health: 10r/m, burst=5
- Headers de seguridad: X-Content-Type-Options, X-Frame-Options
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
27
database.py
27
database.py
@@ -162,23 +162,20 @@ async def crear_invite_code(codigo: str) -> None:
|
||||
|
||||
async def verificar_y_consumir_invite_code(codigo: str, chat_id: int) -> bool:
|
||||
"""
|
||||
Verifica que el código existe y no fue usado, lo marca como consumido y
|
||||
retorna True. Retorna False si no existe o ya fue usado.
|
||||
Operación atómica en una sola transacción SQLite.
|
||||
Verifica y consume un código de invitación en una sola operación atómica.
|
||||
|
||||
Usa UPDATE con condición AND usado=0 en vez de SELECT+UPDATE para eliminar la
|
||||
race condition TOCTOU: dos requests concurrentes con el mismo código solo
|
||||
pueden actualizar la fila una vez — el segundo UPDATE afecta 0 rows y retorna False.
|
||||
"""
|
||||
conn = _assert_conn()
|
||||
async with conn.execute(
|
||||
"SELECT usado FROM invite_codes WHERE codigo = ?", (codigo,)
|
||||
) as cur:
|
||||
row = await cur.fetchone()
|
||||
|
||||
if row is None or row["usado"]:
|
||||
return False
|
||||
|
||||
await conn.execute(
|
||||
"UPDATE invite_codes SET usado = 1, usado_por = ?, usado_at = ? WHERE codigo = ?",
|
||||
cur = await conn.execute(
|
||||
"UPDATE invite_codes SET usado = 1, usado_por = ?, usado_at = ? "
|
||||
"WHERE codigo = ? AND usado = 0",
|
||||
(chat_id, _now_iso(), codigo),
|
||||
)
|
||||
await conn.commit()
|
||||
logger.info("invite_code_consumido", chat_id=chat_id)
|
||||
return True
|
||||
if cur.rowcount > 0:
|
||||
logger.info("invite_code_consumido", chat_id=chat_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user