fix(security): scope CalDAV PUT to its calendar, block iCal SSRF, auth avatar endpoint
- dav_router: PUT now looks up the event within the authenticated calendar only
(local_events.uid is globally unique), so a CalDAV client can no longer
overwrite another user's/calendar's event; a cross-calendar UID clash returns
409 instead of a 500 from the UNIQUE constraint.
- ical_router: _fetch_ics validates the URL (http/https only), resolves the host
and rejects private/loopback/link-local/reserved targets, follows redirects
manually re-validating each hop, and caps the response size — closing an
authenticated SSRF into internal services / cloud metadata.
- profile_router: GET /profile/avatar/{user_id} now requires authentication.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -331,13 +331,22 @@ def _handle_put(cal: models.LocalCalendar, resource: str, body: bytes, db: Sessi
|
||||
name = resource.rsplit("/", 1)[-1]
|
||||
uid = unquote(name[:-4] if name.endswith(".ics") else name) or str(uuid.uuid4())
|
||||
|
||||
# Scope to THIS calendar — never touch another calendar's/user's event that
|
||||
# happens to share the UID (local_events.uid is globally unique).
|
||||
ev = (
|
||||
db.query(models.LocalEvent)
|
||||
.filter(models.LocalEvent.uid == uid)
|
||||
.filter(
|
||||
models.LocalEvent.calendar_id == cal.id,
|
||||
models.LocalEvent.uid == uid,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
created = ev is None
|
||||
if created:
|
||||
# If the UID already exists elsewhere, the global UNIQUE constraint would
|
||||
# 500 on commit — reject cleanly with 409 instead.
|
||||
if db.query(models.LocalEvent.id).filter(models.LocalEvent.uid == uid).first():
|
||||
return Response(status_code=409)
|
||||
ev = models.LocalEvent(calendar_id=cal.id, uid=uid, creator_id=cal.user_id)
|
||||
db.add(ev)
|
||||
ev.title = item.get("title") or "(ohne Titel)"
|
||||
|
||||
Reference in New Issue
Block a user