From 52a0131b56b3cd492718454f7b981a63302b33bc Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Mon, 27 Apr 2026 16:40:01 +0800 Subject: [PATCH] fix(git): LFS token validation and remove IP rate limiting - Implement proper token validation via user_token table (SHA256+base64 hash) - Query token_hash, check IsRevoked, validate expiry - Remove IP-based rate limiting (handled by k8s ingress) - Remove unused client_ip() helper function - user_uid() now async and queries database for real user --- libs/git/http/lfs_routes.rs | 86 +++++++++++++++---------------------- 1 file changed, 35 insertions(+), 51 deletions(-) diff --git a/libs/git/http/lfs_routes.rs b/libs/git/http/lfs_routes.rs index ea6cf89..ced9f03 100644 --- a/libs/git/http/lfs_routes.rs +++ b/libs/git/http/lfs_routes.rs @@ -4,6 +4,10 @@ use crate::http::handler::is_valid_lfs_oid; use crate::http::lfs::{BatchRequest, CreateLockRequest, LfsHandler}; use crate::http::utils::get_repo_model; use actix_web::{Error, HttpRequest, HttpResponse, web}; +use base64::Engine; +use models::users::user_token; +use sea_orm::prelude::*; +use sha2::{Digest, Sha256}; use std::path::PathBuf; fn base_url(req: &HttpRequest) -> String { @@ -28,10 +32,19 @@ fn bearer_token(req: &HttpRequest) -> Result { } } +fn hash_token(token: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(token.as_bytes()); + base64::prelude::BASE64_STANDARD.encode(hasher.finalize()) +} + /// Derive the acting user from the authenticated bearer token, not from a /// client-supplied header. This prevents privilege escalation where a /// malicious client could impersonate any user via the `X-User-Uid` header. -fn user_uid(req: &HttpRequest, repo: &models::repos::repo::Model) -> Result { +async fn user_uid( + req: &HttpRequest, + db: &db::database::AppDatabase, +) -> Result { let auth_header = req .headers() .get("authorization") @@ -43,20 +56,26 @@ fn user_uid(req: &HttpRequest, repo: &models::repos::repo::Model) -> Result() - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token")) -} + let token_hash = hash_token(token); -fn client_ip(req: &HttpRequest) -> String { - req.connection_info() - .realip_remote_addr() - .unwrap_or("unknown") - .to_string() + let token_model = user_token::Entity::find() + .filter(user_token::Column::TokenHash.eq(&token_hash)) + .filter(user_token::Column::IsRevoked.eq(false)) + .one(db.reader()) + .await + .map_err(|e| actix_web::error::ErrorInternalServerError(e.to_string()))?; + + let token_model = token_model + .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid token"))?; + + // Check expiry + if let Some(expires_at) = token_model.expires_at { + if expires_at < chrono::Utc::now() { + return Err(actix_web::error::ErrorUnauthorized("Token expired")); + } + } + + Ok(token_model.user) } pub async fn lfs_batch( @@ -67,13 +86,6 @@ pub async fn lfs_batch( ) -> Result { let (namespace, repo_name) = path.into_inner(); - let ip = client_ip(&req); - if !state.rate_limiter.is_ip_read_allowed(&ip).await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); @@ -99,13 +111,6 @@ pub async fn lfs_upload( return Err(actix_web::error::ErrorBadRequest("Invalid OID format")); } - let ip = client_ip(&req); - if !state.rate_limiter.is_ip_write_allowed(&ip).await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; let token = bearer_token(&req)?; let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); @@ -129,13 +134,6 @@ pub async fn lfs_download( return Err(actix_web::error::ErrorBadRequest("Invalid OID format")); } - let ip = client_ip(&req); - if !state.rate_limiter.is_ip_read_allowed(&ip).await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; let token = bearer_token(&req)?; let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); @@ -156,15 +154,8 @@ pub async fn lfs_lock_create( ) -> Result { let (namespace, repo_name) = path.into_inner(); - let ip = client_ip(&req); - if !state.rate_limiter.is_ip_write_allowed(&ip).await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - let uid = user_uid(&req, &repo)?; + let uid = user_uid(&req, &state.db).await?; let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); match handler.lock_object(&body.oid, uid).await { @@ -215,15 +206,8 @@ pub async fn lfs_lock_delete( ) -> Result { let (namespace, repo_name, lock_id) = path.into_inner(); - let ip = client_ip(&req); - if !state.rate_limiter.is_ip_write_allowed(&ip).await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - let uid = user_uid(&req, &repo)?; + let uid = user_uid(&req, &state.db).await?; let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); match handler.unlock_object(&lock_id, uid).await {