fix(security): stop private-event leak in merge read + harden busy masking, uploads, profile
Findings from the security review:
- HIGH: private local events leaked in full (title/location/description) to
anyone who could READ a shared or group calendar via GET /api/caldav/events —
the private_event_visibility rule was only enforced in /groups/{id}/combined.
Now enforced in the merge read too, via a shared helper (apply_event_privacy)
so the two paths can't drift.
- HIGH: 'busy' masking was a blacklist that still leaked creator identity,
source-calendar name, recurrence rule and per-event colour. Replaced with a
whitelist (mask_busy_event): only timing/identity/render fields survive.
- MEDIUM: .ics import had no size limit (raw = await file.read()) → memory DoS.
Now capped at 5 MB (413), read before creating any calendar.
- LOW/INFO: profile email now checked for uniqueness + basic format; display
name / username / email length-capped and control-chars stripped.
Deferred (tracked): RRULE expansion cap at the trust boundary, SQLite
PRAGMA foreign_keys + ON DELETE cascade, and JWT-by-user-id + token version.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ from sqlalchemy.orm import Session
|
||||
import models
|
||||
from auth import get_current_user
|
||||
from database import get_db
|
||||
from local_events_util import build_local_event_dict, expand_recurring_local
|
||||
from local_events_util import build_local_event_dict, expand_recurring_local, mask_busy_event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -309,16 +309,6 @@ def delete_group(
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _strip_busy(event: dict) -> dict:
|
||||
"""Anonymise a private event for the 'busy' visibility mode."""
|
||||
event = dict(event)
|
||||
event["title"] = "Beschäftigt"
|
||||
event["location"] = ""
|
||||
event["description"] = ""
|
||||
event["private"] = True
|
||||
return event
|
||||
|
||||
|
||||
def _first_name(name: Optional[str]) -> str:
|
||||
if not name:
|
||||
return ""
|
||||
@@ -415,7 +405,7 @@ def combined_events(
|
||||
|
||||
for b in built:
|
||||
if ev.is_private and creator_owner_id != current_user.id and visibility_for(creator_owner_id) == "busy":
|
||||
b = _strip_busy(b)
|
||||
b = mask_busy_event(b)
|
||||
# Colour to render with: the group calendar's colour for group
|
||||
# events, otherwise the owning member's group colour.
|
||||
b["display_color"] = group_cal_color if is_group else member_color.get(owner_id)
|
||||
|
||||
Reference in New Issue
Block a user