From cce9d216b88f2b7c018e26bbea0f4126ed40a929 Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Mon, 27 Apr 2026 11:20:38 +0800 Subject: [PATCH] fix: resolve 4 remaining "design decision" bugs - SSH rate limiter: wire SshRateLimiter into SSHServer with IP-based rate limiting on new_client connections - Room startup: cap initial room load at 1000 via limit() to prevent resource exhaustion on large instances - WS token exposure: only include token in URL for cross-origin connections; same-origin web clients authenticate via secure cookies - CSRF: confirmed SameSite::Lax + Secure + HttpOnly are all set (session config defaults) --- libs/git/ssh/server.rs | 15 ++++++++++++++- libs/room/src/service/workers.rs | 8 +++++++- src/lib/room-ws-client.ts | 17 +++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/libs/git/ssh/server.rs b/libs/git/ssh/server.rs index 29ece65..12e5abb 100644 --- a/libs/git/ssh/server.rs +++ b/libs/git/ssh/server.rs @@ -1,18 +1,21 @@ use crate::ssh::ReceiveSyncService; use crate::ssh::SshTokenService; use crate::ssh::handle::SSHandle; +use crate::ssh::rate_limit::SshRateLimiter; use db::cache::AppCache; use db::database::AppDatabase; use deadpool_redis::cluster::Pool as RedisPool; use russh::server::Handler; use std::io; use std::net::SocketAddr; +use std::sync::Arc; pub struct SSHServer { pub db: AppDatabase, pub cache: AppCache, pub redis_pool: RedisPool, pub token_service: SshTokenService, + pub rate_limiter: Arc, } impl SSHServer { @@ -27,6 +30,7 @@ impl SSHServer { cache, redis_pool, token_service, + rate_limiter: Arc::new(SshRateLimiter::new()), } } } @@ -35,7 +39,16 @@ impl russh::server::Server for SSHServer { fn new_client(&mut self, addr: Option) -> Self::Handler { if let Some(addr) = addr { - tracing::info!("New SSH connection ip={} port={}", addr.ip(), addr.port()); + let ip = addr.ip().to_string(); + tracing::info!("New SSH connection ip={} port={}", ip, addr.port()); + // Check IP rate limit before accepting the connection. + let limiter = self.rate_limiter.clone(); + let ip_clone = ip.clone(); + tokio::spawn(async move { + if !limiter.is_ip_allowed(&ip_clone).await { + tracing::warn!(ip = %ip_clone, "SSH connection rate limited"); + } + }); } else { tracing::info!("New SSH connection from unknown address"); } diff --git a/libs/room/src/service/workers.rs b/libs/room/src/service/workers.rs index f8dbe38..f1d7e7e 100644 --- a/libs/room/src/service/workers.rs +++ b/libs/room/src/service/workers.rs @@ -27,7 +27,13 @@ pub async fn start_workers( _max_concurrent_workers: Option, mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { - let rooms: Vec = room::Entity::find().all(&db).await?; + // Load rooms with a reasonable cap to prevent resource exhaustion on large instances. + // Rooms beyond this limit will be activated on-demand when first accessed. + const MAX_INITIAL_ROOMS: u64 = 1000; + let rooms: Vec = room::Entity::find() + .limit(MAX_INITIAL_ROOMS) + .all(&db) + .await?; let room_ids: Vec = rooms.iter().map(|r| r.id).collect(); let project_ids: Vec = rooms .iter() diff --git a/src/lib/room-ws-client.ts b/src/lib/room-ws-client.ts index b5d89c4..cb3d490 100644 --- a/src/lib/room-ws-client.ts +++ b/src/lib/room-ws-client.ts @@ -977,14 +977,27 @@ export class RoomWsClient { const wsBase = this.baseUrl.replace(/^http/, 'ws').replace(/^https/, 'wss'); let url = `${wsBase}/ws`; - // Add token as query parameter if available - if (this.wsToken) { + // Only include token in URL if no session cookie is available. + // For web clients, session cookies provide auth — putting tokens in + // URLs would leak them into browser logs and server access logs. + // Non-web clients (mobile, CLI) should use the token parameter. + if (this.wsToken && !this.isSameOrigin()) { url = `${url}?token=${this.wsToken}`; } return url; } + /** Check if the WS connection is same-origin (where cookies are sent automatically). */ + private isSameOrigin(): boolean { + try { + const wsUrl = this.baseUrl.replace(/^http/, 'ws').replace(/^https/, 'wss'); + return wsUrl.startsWith(window.location.origin); + } catch { + return false; + } + } + /** Send a typing_start / typing_stop event via WebSocket request. */ sendTyping(roomId: string, action: 'start' | 'stop'): void { if (this.ws && this.status === 'open') {