Skip to content

Commit f38c36a

Browse files
author
zocomputer
committed
security: Rate limiting on TOTP, session revocation, and attachment deletion
1 parent 3947610 commit f38c36a

3 files changed

Lines changed: 130 additions & 2 deletions

File tree

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ use serde::Serialize;
1818
use std::sync::Arc;
1919
use uuid::Uuid;
2020

21-
use crate::{middleware::AuthContext, AppState};
21+
use crate::{
22+
middleware::{AuthContext, check_rate_limit_with_fallback, extract_client_ip, HeaderMap},
23+
AppState,
24+
};
2225

2326
pub fn router() -> Router<Arc<AppState>> {
2427
Router::new()
@@ -59,6 +62,21 @@ async fn revoke_session(
5962
State(state): State<Arc<AppState>>,
6063
Path(session_id_str): Path<String>,
6164
) -> NexusResult<StatusCode> {
65+
// Rate limiting: 10 session revocations per user per 5 minutes
66+
let ip = extract_client_ip(&headers);
67+
check_rate_limit_with_fallback(
68+
state.db.redis.as_ref(),
69+
format!("rl:session_revoke:user:{}", auth_ctx.user_id),
70+
10,
71+
300,
72+
).await?;
73+
check_rate_limit_with_fallback(
74+
state.db.redis.as_ref(),
75+
format!("rl:session_revoke:ip:{ip}"),
76+
20,
77+
300,
78+
).await?;
79+
6280
let session_id: Uuid = session_id_str
6381
.parse()
6482
.map_err(|_| NexusError::Validation { message: "Invalid session ID format".into() })?;
@@ -82,6 +100,21 @@ async fn revoke_all_sessions(
82100
Extension(auth_ctx): Extension<AuthContext>,
83101
State(state): State<Arc<AppState>>,
84102
) -> NexusResult<Json<RevokedCountResponse>> {
103+
// Rate limiting: 5 bulk session revocations per user per hour (high impact operation)
104+
let ip = extract_client_ip(&headers);
105+
check_rate_limit_with_fallback(
106+
state.db.redis.as_ref(),
107+
format!("rl:session_revoke_all:user:{}", auth_ctx.user_id),
108+
5,
109+
3600,
110+
).await?;
111+
check_rate_limit_with_fallback(
112+
state.db.redis.as_ref(),
113+
format!("rl:session_revoke_all:ip:{ip}"),
114+
10,
115+
3600,
116+
).await?;
117+
85118
// Use the JTI from the access token claims to exclude the current session.
86119
let current_session_id = auth_ctx.session_id.unwrap_or(Uuid::nil());
87120

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ use totp_rs::{Algorithm, Secret, TOTP};
2323

2424
use chrono::{Duration, Utc};
2525

26-
use crate::{auth, middleware::AuthContext, AppState};
26+
use crate::{
27+
auth,
28+
middleware::{AuthContext, check_rate_limit_with_fallback, extract_client_ip, HeaderMap},
29+
AppState,
30+
};
2731

2832
// ── Constants ────────────────────────────────────────────────────────────────
2933

@@ -109,6 +113,21 @@ async fn setup(
109113
Extension(auth_ctx): Extension<AuthContext>,
110114
State(state): State<Arc<AppState>>,
111115
) -> NexusResult<Json<SetupResponse>> {
116+
// Rate limiting: 5 TOTP setup attempts per user per hour (prevents enumeration)
117+
let ip = extract_client_ip(&headers);
118+
check_rate_limit_with_fallback(
119+
state.db.redis.as_ref(),
120+
format!("rl:2fa:setup:user:{}", auth.user_id),
121+
5,
122+
3600,
123+
).await?;
124+
check_rate_limit_with_fallback(
125+
state.db.redis.as_ref(),
126+
format!("rl:2fa:setup:ip:{ip}"),
127+
10,
128+
3600,
129+
).await?;
130+
112131
let user = users::find_by_id(&state.db.pool, auth_ctx.user_id)
113132
.await?
114133
.ok_or(NexusError::NotFound { resource: "User".into() })?;
@@ -167,6 +186,21 @@ async fn enable(
167186
State(state): State<Arc<AppState>>,
168187
Json(body): Json<EnableBody>,
169188
) -> NexusResult<()> {
189+
// Rate limiting: 10 TOTP enable attempts per user per 10 minutes (prevents brute force on verification code)
190+
let ip = extract_client_ip(&headers);
191+
check_rate_limit_with_fallback(
192+
state.db.redis.as_ref(),
193+
format!("rl:2fa:enable:user:{}", auth.user_id),
194+
10,
195+
600,
196+
).await?;
197+
check_rate_limit_with_fallback(
198+
state.db.redis.as_ref(),
199+
format!("rl:2fa:enable:ip:{ip}"),
200+
20,
201+
600,
202+
).await?;
203+
170204
let user = users::find_by_id(&state.db.pool, auth_ctx.user_id)
171205
.await?
172206
.ok_or(NexusError::NotFound { resource: "User".into() })?;
@@ -214,6 +248,21 @@ async fn disable(
214248
State(state): State<Arc<AppState>>,
215249
Json(body): Json<DisableBody>,
216250
) -> NexusResult<()> {
251+
// Rate limiting: 10 TOTP disable attempts per user per 10 minutes
252+
let ip = extract_client_ip(&headers);
253+
check_rate_limit_with_fallback(
254+
state.db.redis.as_ref(),
255+
format!("rl:2fa:disable:user:{}", auth.user_id),
256+
10,
257+
600,
258+
).await?;
259+
check_rate_limit_with_fallback(
260+
state.db.redis.as_ref(),
261+
format!("rl:2fa:disable:ip:{ip}"),
262+
20,
263+
600,
264+
).await?;
265+
217266
let user = users::find_by_id(&state.db.pool, auth_ctx.user_id)
218267
.await?
219268
.ok_or(NexusError::NotFound { resource: "User".into() })?;
@@ -286,6 +335,22 @@ async fn verify_mfa(
286335
State(state): State<Arc<AppState>>,
287336
Json(body): Json<VerifyMfaBody>,
288337
) -> NexusResult<Json<AuthResponse>> {
338+
// CRITICAL: Rate limiting on MFA verification to prevent brute force attacks
339+
// 5 attempts per user per 5 minutes (stricter than other endpoints)
340+
let ip = extract_client_ip(&headers);
341+
check_rate_limit_with_fallback(
342+
state.db.redis.as_ref(),
343+
format!("rl:2fa:verify:user:{}", body.user_id),
344+
5,
345+
300,
346+
).await?;
347+
check_rate_limit_with_fallback(
348+
state.db.redis.as_ref(),
349+
format!("rl:2fa:verify:ip:{ip}"),
350+
10,
351+
300,
352+
).await?;
353+
289354
let config = nexus_common::config::get();
290355

291356
// Validate the challenge token
@@ -400,6 +465,21 @@ async fn regenerate_backup_codes(
400465
State(state): State<Arc<AppState>>,
401466
Json(body): Json<RegenerateBody>,
402467
) -> NexusResult<Json<RegenerateResponse>> {
468+
// Rate limiting: 3 backup code regenerations per user per hour
469+
let ip = extract_client_ip(&headers);
470+
check_rate_limit_with_fallback(
471+
state.db.redis.as_ref(),
472+
format!("rl:2fa:regen:user:{}", auth.user_id),
473+
3,
474+
3600,
475+
).await?;
476+
check_rate_limit_with_fallback(
477+
state.db.redis.as_ref(),
478+
format!("rl:2fa:regen:ip:{ip}"),
479+
5,
480+
3600,
481+
).await?;
482+
403483
let user = users::find_by_id(&state.db.pool, auth_ctx.user_id)
404484
.await?
405485
.ok_or(NexusError::NotFound { resource: "User".into() })?;

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,21 @@ async fn delete_attachment(
315315
State(state): State<Arc<AppState>>,
316316
Path(id): Path<Uuid>,
317317
) -> NexusResult<Json<serde_json::Value>> {
318+
// Rate limiting: 30 attachment deletions per user per 5 minutes
319+
let ip = extract_client_ip(&headers);
320+
check_rate_limit_with_fallback(
321+
state.db.redis.as_ref(),
322+
format!("rl:attachment_delete:user:{}", auth.user_id),
323+
30,
324+
300,
325+
).await?;
326+
check_rate_limit_with_fallback(
327+
state.db.redis.as_ref(),
328+
format!("rl:attachment_delete:ip:{ip}"),
329+
50,
330+
300,
331+
).await?;
332+
318333
// Find the attachment first to get the storage key
319334
let row = attachments::find_by_id(&state.db.pool, id)
320335
.await?

0 commit comments

Comments
 (0)