Fix 2 pentest findings: unlock permission check + permanent lock preservation

SC-01: unlock_event now verifies caller has access to the calendar before
revealing lock state. Previously any authenticated user could probe event
existence via 404/204/403 response differences.

SC-02: acquire_lock no longer overwrites permanent locks. If the owner holds
a permanent lock and clicks Edit, the existing lock is returned as-is instead
of being downgraded to a 5-minute temporary lock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-06 23:37:05 +08:00
parent 8f777dd15a
commit 206144d20d
2 changed files with 17 additions and 0 deletions

View File

@ -716,6 +716,11 @@ async def unlock_event(
if not event: if not event:
raise HTTPException(status_code=404, detail="Event not found") raise HTTPException(status_code=404, detail="Event not found")
# SC-01: Verify caller has access to this calendar before revealing lock state
perm = await get_user_permission(db, event.calendar_id, current_user.id)
if perm is None:
raise HTTPException(status_code=404, detail="Event not found")
lock_result = await db.execute( lock_result = await db.execute(
select(EventLock).where(EventLock.event_id == event_id) select(EventLock).where(EventLock.event_id == event_id)
) )

View File

@ -66,8 +66,20 @@ async def acquire_lock(db: AsyncSession, event_id: int, user_id: int) -> EventLo
""" """
Atomic INSERT ON CONFLICT acquires a 5-minute lock on the event. Atomic INSERT ON CONFLICT acquires a 5-minute lock on the event.
Only succeeds if no unexpired lock exists or the existing lock is held by the same user. Only succeeds if no unexpired lock exists or the existing lock is held by the same user.
Permanent locks are never overwritten if the same user holds one, it is returned as-is.
Returns the lock or raises 423 Locked. Returns the lock or raises 423 Locked.
""" """
# Check for existing permanent lock first
existing = await db.execute(
select(EventLock).where(EventLock.event_id == event_id)
)
existing_lock = existing.scalar_one_or_none()
if existing_lock and existing_lock.is_permanent:
if existing_lock.locked_by == user_id:
# Owner holds permanent lock — return it without downgrading
return existing_lock
raise HTTPException(status_code=423, detail="Event is permanently locked by the calendar owner")
now = datetime.now() now = datetime.now()
expires = now + timedelta(minutes=LOCK_DURATION_MINUTES) expires = now + timedelta(minutes=LOCK_DURATION_MINUTES)