Skip to content

Commit 3947610

Browse files
author
zocomputer
committed
security: Rate limiting on all moderation endpoints (kick/ban/timeout/report/word-filters)
1 parent 3877417 commit 3947610

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

crates/nexus-api/src/routes/moderation.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use std::sync::Arc;
2525
use uuid::Uuid;
2626

2727
use crate::{middleware::AuthContext, AppState};
28+
use crate::middleware::{HeaderMap, check_rate_limit_with_fallback, extract_client_ip, USER_AGENT};
2829

2930
/// All moderation routes (require authentication).
3031
pub fn router() -> Router<Arc<AppState>> {
@@ -179,6 +180,21 @@ async fn kick_member(
179180
require_server_permission(&state.db.pool, &server, auth.user_id, Permissions::KICK_MEMBERS)
180181
.await?;
181182

183+
// Rate limiting: 10 kicks per user per 5 minutes
184+
let ip = extract_client_ip(&headers);
185+
check_rate_limit_with_fallback(
186+
state.db.redis.as_ref(),
187+
format!("rl:kick:user:{}", auth.user_id),
188+
10,
189+
300,
190+
).await?;
191+
check_rate_limit_with_fallback(
192+
state.db.redis.as_ref(),
193+
format!("rl:kick:ip:{ip}"),
194+
20,
195+
300,
196+
).await?;
197+
182198
// Can't kick the owner
183199
if target_id == server.owner_id {
184200
return Err(NexusError::Forbidden);
@@ -245,6 +261,21 @@ async fn ban_member(
245261
require_server_permission(&state.db.pool, &server, auth.user_id, Permissions::BAN_MEMBERS)
246262
.await?;
247263

264+
// Rate limiting: 10 bans per user per 5 minutes
265+
let ip = extract_client_ip(&headers);
266+
check_rate_limit_with_fallback(
267+
state.db.redis.as_ref(),
268+
format!("rl:ban:user:{}", auth.user_id),
269+
10,
270+
300,
271+
).await?;
272+
check_rate_limit_with_fallback(
273+
state.db.redis.as_ref(),
274+
format!("rl:ban:ip:{ip}"),
275+
20,
276+
300,
277+
).await?;
278+
248279
if target_id == server.owner_id {
249280
return Err(NexusError::Forbidden);
250281
}
@@ -307,6 +338,21 @@ async fn unban_member(
307338
require_server_permission(&state.db.pool, &server, auth.user_id, Permissions::BAN_MEMBERS)
308339
.await?;
309340

341+
// Rate limiting: 10 unbans per user per 5 minutes
342+
let ip = extract_client_ip(&headers);
343+
check_rate_limit_with_fallback(
344+
state.db.redis.as_ref(),
345+
format!("rl:unban:user:{}", auth.user_id),
346+
10,
347+
300,
348+
).await?;
349+
check_rate_limit_with_fallback(
350+
state.db.redis.as_ref(),
351+
format!("rl:unban:ip:{ip}"),
352+
20,
353+
300,
354+
).await?;
355+
310356
let removed = moderation::remove_ban(&state.db.pool, target_id, server_id).await?;
311357
if !removed {
312358
return Err(NexusError::NotFound { resource: "Ban".into() });
@@ -376,6 +422,21 @@ async fn set_timeout(
376422
require_server_permission(&state.db.pool, &server, auth.user_id, Permissions::KICK_MEMBERS)
377423
.await?;
378424

425+
// Rate limiting: 10 timeouts per user per 5 minutes
426+
let ip = extract_client_ip(&headers);
427+
check_rate_limit_with_fallback(
428+
state.db.redis.as_ref(),
429+
format!("rl:timeout:user:{}", auth.user_id),
430+
10,
431+
300,
432+
).await?;
433+
check_rate_limit_with_fallback(
434+
state.db.redis.as_ref(),
435+
format!("rl:timeout:ip:{ip}"),
436+
20,
437+
300,
438+
).await?;
439+
379440
if target_id == server.owner_id {
380441
return Err(NexusError::Forbidden);
381442
}
@@ -601,6 +662,21 @@ async fn resolve_report(
601662
Path((server_id, report_id)): Path<(Uuid, Uuid)>,
602663
Json(body): Json<ResolveReportBody>,
603664
) -> NexusResult<Json<serde_json::Value>> {
665+
// Rate limiting: 20 report resolutions per user per 5 minutes
666+
let ip = extract_client_ip(&headers);
667+
check_rate_limit_with_fallback(
668+
state.db.redis.as_ref(),
669+
format!("rl:report_resolve:user:{}", auth.user_id),
670+
20,
671+
300,
672+
).await?;
673+
check_rate_limit_with_fallback(
674+
state.db.redis.as_ref(),
675+
format!("rl:report_resolve:ip:{ip}"),
676+
40,
677+
300,
678+
).await?;
679+
604680
let server = get_server_or_404(&state.db.pool, server_id).await?;
605681
require_server_permission(
606682
&state.db.pool,
@@ -642,6 +718,21 @@ async fn dismiss_report(
642718
State(state): State<Arc<AppState>>,
643719
Path((server_id, report_id)): Path<(Uuid, Uuid)>,
644720
) -> NexusResult<Json<serde_json::Value>> {
721+
// Rate limiting: 20 report dismissals per user per 5 minutes
722+
let ip = extract_client_ip(&headers);
723+
check_rate_limit_with_fallback(
724+
state.db.redis.as_ref(),
725+
format!("rl:report_dismiss:user:{}", auth.user_id),
726+
20,
727+
300,
728+
).await?;
729+
check_rate_limit_with_fallback(
730+
state.db.redis.as_ref(),
731+
format!("rl:report_dismiss:ip:{ip}"),
732+
40,
733+
300,
734+
).await?;
735+
645736
let server = get_server_or_404(&state.db.pool, server_id).await?;
646737
require_server_permission(
647738
&state.db.pool,
@@ -708,6 +799,21 @@ async fn add_word_filter(
708799
Path(server_id): Path<Uuid>,
709800
Json(body): Json<AddFilterBody>,
710801
) -> NexusResult<Json<moderation::WordFilter>> {
802+
// Rate limiting: 10 word filter changes per user per 5 minutes
803+
let ip = extract_client_ip(&headers);
804+
check_rate_limit_with_fallback(
805+
state.db.redis.as_ref(),
806+
format!("rl:word_filter:user:{}", auth.user_id),
807+
10,
808+
300,
809+
).await?;
810+
check_rate_limit_with_fallback(
811+
state.db.redis.as_ref(),
812+
format!("rl:word_filter:ip:{ip}"),
813+
20,
814+
300,
815+
).await?;
816+
711817
let pattern = body.pattern.trim().to_lowercase();
712818
if pattern.is_empty() {
713819
return Err(NexusError::Validation { message: "Pattern cannot be empty".into() });
@@ -771,6 +877,21 @@ async fn remove_word_filter(
771877
State(state): State<Arc<AppState>>,
772878
Path((server_id, filter_id)): Path<(Uuid, Uuid)>,
773879
) -> NexusResult<Json<serde_json::Value>> {
880+
// Rate limiting: 10 word filter changes per user per 5 minutes
881+
let ip = extract_client_ip(&headers);
882+
check_rate_limit_with_fallback(
883+
state.db.redis.as_ref(),
884+
format!("rl:word_filter:user:{}", auth.user_id),
885+
10,
886+
300,
887+
).await?;
888+
check_rate_limit_with_fallback(
889+
state.db.redis.as_ref(),
890+
format!("rl:word_filter:ip:{ip}"),
891+
20,
892+
300,
893+
).await?;
894+
774895
let server = get_server_or_404(&state.db.pool, server_id).await?;
775896
require_server_permission(&state.db.pool, &server, auth.user_id, Permissions::MANAGE_SERVER)
776897
.await?;

0 commit comments

Comments
 (0)