fix(security): TrustedHostMiddleware + escape UID/RRULE iCal + tests deterministas del flag

- TrustedHostMiddleware (127.0.0.1/localhost/testserver): anti DNS-rebinding, cierra el
  vector por el que una web maliciosa alcanza el service local desde el navegador.
- _build_vcalendar: sanitiza UID y RRULE (quita saltos de línea) para evitar iCal injection
  (summary/location/description ya escapaban con _vcard_escape).
- tests: fixture autouse que fuerza OSINT_DB_BACKEND OFF por defecto, así la suite es
  determinista sin depender del estado real de dev/feature_flags.json (los tests del camino
  ON sobrescriben _FLAGS_FILE). Corrige 14 fallos que aparecían con el flag activado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 01:22:36 +02:00
parent 83c672c072
commit 3716b3f22a
3 changed files with 28 additions and 3 deletions
+16 -2
View File
@@ -2199,7 +2199,9 @@ def _build_vcalendar(data: "EventIn", uid: str) -> str:
body.append(vtz)
vevent = [
"BEGIN:VEVENT",
"UID:%s" % uid,
# Sanitizamos el UID (quitamos saltos de línea) para que no pueda inyectar
# propiedades/componentes iCal nuevos en el VEVENT.
"UID:%s" % str(uid).replace("\r", "").replace("\n", ""),
"DTSTAMP:%s" % dtstamp,
_ical_dt_property("DTSTART", data.dtstart, tz, data.all_day),
]
@@ -2219,7 +2221,10 @@ def _build_vcalendar(data: "EventIn", uid: str) -> str:
# canónica "RRULE:<cuerpo>" que entienden Xandikos y los clientes (DAVx5).
if rrule.upper().startswith("RRULE:"):
rrule = rrule[len("RRULE:"):].strip()
vevent.append("RRULE:%s" % rrule)
# Sanitizar: quitar saltos de línea para que el valor de la RRULE no
# inyecte propiedades/componentes nuevos (los `;`/`,` son separadores
# legítimos de la regla, así que no se escapan).
vevent.append("RRULE:%s" % rrule.replace("\r", "").replace("\n", ""))
vevent.append("END:VEVENT")
body.append("\r\n".join(vevent))
body.append("END:VCALENDAR")
@@ -2240,6 +2245,15 @@ def create_app(vault_dir: str) -> FastAPI:
"""
state = VaultState(vault_dir)
app = FastAPI(title="osint_web", version="0.1.0")
# Anti DNS-rebinding: solo acepta requests cuyo Host sea localhost. Cierra el
# vector por el que una web maliciosa rebindea su dominio a 127.0.0.1 y, desde
# el navegador del usuario, alcanza este service local (sin auth) o el de DuckDB.
from starlette.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["127.0.0.1", "localhost", "testserver"],
)
app.state.vault = state
# -- Vault --