fix(security): high — live authz, XFF, SSE buffering, atomicity, pagination
H1: auth extractor re-reads the live user row — trusts the DB role (not the
JWT claim) and rejects banned users mid-session. e2e proves a demoted
host loses powers and a banned host is locked out and can't self-unban.
H2: client_ip takes the right-most XFF hop (the one Caddy appends); spoofed
left-most entries are ignored, restoring IP-based throttles.
H3: Caddy excludes /api/v1/stream from `encode` so SSE isn't buffered.
H4: upload — quota increment + row insert + hashtag links now one txn.
H5: feed keyset pagination tiebroken on (created_at, id) + composite index
(migration 010); feed_delta bounded with LIMIT.
H7: LightboxModal keys its comment load off upload.id, ending the
SSE-driven refetch storm.
H8: CSP via SvelteKit kit.csp (svelte.config.js) hardens the localStorage
token model against injection.
Migration 010 also adds the latent comment/comment_hashtag indexes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -190,25 +190,6 @@ pub async fn upload(
|
||||
}
|
||||
tokio::fs::write(&absolute_path, &data).await.map_err(|e| AppError::Internal(e.into()))?;
|
||||
|
||||
// Update user's total upload bytes
|
||||
sqlx::query("UPDATE \"user\" SET total_upload_bytes = total_upload_bytes + $2 WHERE id = $1")
|
||||
.bind(auth.user_id)
|
||||
.bind(size)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Insert upload record
|
||||
let upload = Upload::create(
|
||||
&state.pool,
|
||||
auth.event_id,
|
||||
auth.user_id,
|
||||
&relative_path,
|
||||
&mime,
|
||||
size,
|
||||
caption.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Process hashtags from caption and explicit CSV
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
if let Some(ref cap) = caption {
|
||||
@@ -225,10 +206,30 @@ pub async fn upload(
|
||||
tags.sort();
|
||||
tags.dedup();
|
||||
|
||||
// Quota accounting, the upload row, and its hashtag links must be atomic: a
|
||||
// crash between the bytes increment and the insert would permanently charge
|
||||
// bytes with no row to reclaim them (silent quota erosion / spurious lockout).
|
||||
let mut tx = state.pool.begin().await?;
|
||||
sqlx::query("UPDATE \"user\" SET total_upload_bytes = total_upload_bytes + $2 WHERE id = $1")
|
||||
.bind(auth.user_id)
|
||||
.bind(size)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let upload = Upload::create(
|
||||
&mut *tx,
|
||||
auth.event_id,
|
||||
auth.user_id,
|
||||
&relative_path,
|
||||
&mime,
|
||||
size,
|
||||
caption.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
for tag in &tags {
|
||||
let h = Hashtag::upsert(&state.pool, auth.event_id, tag).await?;
|
||||
Hashtag::link_to_upload(&state.pool, upload.id, h.id).await?;
|
||||
let h = Hashtag::upsert(&mut *tx, auth.event_id, tag).await?;
|
||||
Hashtag::link_to_upload(&mut *tx, upload.id, h.id).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
// Spawn compression task
|
||||
state
|
||||
@@ -279,17 +280,20 @@ pub async fn edit_upload(
|
||||
return Err(AppError::Forbidden("Nur eigene Uploads bearbeiten.".into()));
|
||||
}
|
||||
|
||||
// Caption update + hashtag wipe-then-relink in one transaction, so a crash
|
||||
// mid-relink can't leave the upload with its hashtags stripped.
|
||||
let mut tx = state.pool.begin().await?;
|
||||
if let Some(ref caption) = body.caption {
|
||||
Upload::update_caption(&state.pool, upload_id, Some(caption)).await?;
|
||||
Upload::update_caption(&mut *tx, upload_id, Some(caption)).await?;
|
||||
}
|
||||
|
||||
if let Some(ref hashtags) = body.hashtags {
|
||||
Hashtag::unlink_all_from_upload(&state.pool, upload_id).await?;
|
||||
Hashtag::unlink_all_from_upload(&mut *tx, upload_id).await?;
|
||||
for tag in hashtags {
|
||||
let h = Hashtag::upsert(&state.pool, auth.event_id, tag).await?;
|
||||
Hashtag::link_to_upload(&state.pool, upload_id, h.id).await?;
|
||||
let h = Hashtag::upsert(&mut *tx, auth.event_id, tag).await?;
|
||||
Hashtag::link_to_upload(&mut *tx, upload_id, h.id).await?;
|
||||
}
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user