-
Notifications
You must be signed in to change notification settings - Fork 95
feat: Implement issues #549, #548, #541 - Secure session management, … #674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ use std::env; | |
| use uuid::Uuid; | ||
|
|
||
| use dto::auth::{RegisterRequest, LoginRequest, AuthResponse, ErrorResponse, RefreshTokenRequest, RefreshResponse, LogoutResponse}; | ||
| use security::{JwtService, TokenService, TokenServiceError}; | ||
| use security::{JwtService, TokenService, TokenServiceError, SessionService}; | ||
| use sea_orm::DatabaseConnection; | ||
|
|
||
| /// Register a new user | ||
|
|
@@ -58,6 +58,7 @@ pub async fn register( | |
| #[post("/login")] | ||
| pub async fn login( | ||
| db: web::Data<DatabaseConnection>, | ||
| req: HttpRequest, | ||
| payload: web::Json<LoginRequest>, | ||
| jwt_service: web::Data<JwtService>, | ||
| ) -> HttpResponse { | ||
|
|
@@ -73,8 +74,31 @@ pub async fn login( | |
| let user_id = 1; | ||
| let username = payload.username.clone(); | ||
|
|
||
| // Generate access token | ||
| let access_token = match jwt_service.generate_token(user_id, &username) { | ||
| // Extract IP address and user agent for session tracking | ||
| let ip_address = req.connection_info().real_ip_remote_addr().map(|s| s.to_string()); | ||
| let user_agent = req.headers().get("User-Agent") | ||
| .and_then(|h| h.to_str().ok()) | ||
| .map(|s| s.to_string()); | ||
|
|
||
| // Create a new session | ||
| let session_ttl = env::var("SESSION_TTL_HOURS") | ||
| .unwrap_or_else(|_| "168".to_string()) // 7 days default | ||
| .parse::<i64>() | ||
| .unwrap_or(168); | ||
|
|
||
| let session_id = match SessionService::create_session(&db, user_id, ip_address, user_agent, session_ttl).await { | ||
| Ok(sid) => sid, | ||
| Err(e) => { | ||
| log::error!("Failed to create session: {}", e); | ||
| return HttpResponse::InternalServerError().json(ErrorResponse { | ||
| message: "Failed to create session".to_string(), | ||
| code: "SESSION_ERROR".to_string(), | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| // Generate access token with session ID | ||
| let access_token = match jwt_service.generate_token_with_session(user_id, &username, Some(session_id.to_string()), None) { | ||
|
Comment on lines
+77
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PR objective explicitly promises "IP binding to mitigate token theft", and Hash the IP (e.g., SHA-256 with a server-side pepper to avoid exposing raw IPs in logs/tokens) and thread it into 🔧 Proposed fix (login handler) // Extract IP address and user agent for session tracking
let ip_address = req.connection_info().real_ip_remote_addr().map(|s| s.to_string());
+ let ip_hash = ip_address.as_deref().map(|ip| {
+ use sha2::{Digest, Sha256};
+ let pepper = env::var("IP_HASH_PEPPER").unwrap_or_default();
+ let mut hasher = Sha256::new();
+ hasher.update(pepper.as_bytes());
+ hasher.update(ip.as_bytes());
+ format!("{:x}", hasher.finalize())
+ });
let user_agent = req.headers().get("User-Agent")
...
- let access_token = match jwt_service.generate_token_with_session(user_id, &username, Some(session_id.to_string()), None) {
+ let access_token = match jwt_service.generate_token_with_session(user_id, &username, Some(session_id.to_string()), ip_hash) {🤖 Prompt for AI Agents |
||
| Ok(t) => t, | ||
| Err(_) => { | ||
| return HttpResponse::InternalServerError().json(ErrorResponse { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,223 @@ | ||||||||||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||||||||||
| use std::env; | ||||||||||||||||||||||||||||
| use std::fmt; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Errors that can occur during PGN archival | ||||||||||||||||||||||||||||
| #[derive(Debug)] | ||||||||||||||||||||||||||||
| pub enum PgnArchiveError { | ||||||||||||||||||||||||||||
| IpfsError(String), | ||||||||||||||||||||||||||||
| ArweaveError(String), | ||||||||||||||||||||||||||||
| SerializationError(String), | ||||||||||||||||||||||||||||
| NetworkError(String), | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| impl fmt::Display for PgnArchiveError { | ||||||||||||||||||||||||||||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||||||||||||||||||||||||
| match self { | ||||||||||||||||||||||||||||
| Self::IpfsError(e) => write!(f, "IPFS error: {}", e), | ||||||||||||||||||||||||||||
| Self::ArweaveError(e) => write!(f, "Arweave error: {}", e), | ||||||||||||||||||||||||||||
| Self::SerializationError(e) => write!(f, "Serialization error: {}", e), | ||||||||||||||||||||||||||||
| Self::NetworkError(e) => write!(f, "Network error: {}", e), | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| impl std::error::Error for PgnArchiveError {} | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Response from IPFS pinning | ||||||||||||||||||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||||||||||||||||||
| pub struct IpfsResponse { | ||||||||||||||||||||||||||||
| #[serde(rename = "IpfsHash")] | ||||||||||||||||||||||||||||
| pub ipfs_hash: String, | ||||||||||||||||||||||||||||
| #[serde(rename = "PinSize")] | ||||||||||||||||||||||||||||
| pub pin_size: usize, | ||||||||||||||||||||||||||||
| #[serde(rename = "Timestamp")] | ||||||||||||||||||||||||||||
| pub timestamp: String, | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Response from Arweave upload | ||||||||||||||||||||||||||||
| #[derive(Debug, Serialize, Deserialize)] | ||||||||||||||||||||||||||||
| pub struct ArweaveResponse { | ||||||||||||||||||||||||||||
| pub id: String, | ||||||||||||||||||||||||||||
| pub status: u16, | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Archive result containing both IPFS and Arweave identifiers | ||||||||||||||||||||||||||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||||||||||||||||||||||||||
| pub struct PgnArchiveResult { | ||||||||||||||||||||||||||||
| pub game_id: String, | ||||||||||||||||||||||||||||
| pub ipfs_cid: Option<String>, | ||||||||||||||||||||||||||||
| pub arweave_tx_id: Option<String>, | ||||||||||||||||||||||||||||
| pub pgn_hash: String, | ||||||||||||||||||||||||||||
| pub archived_at: String, | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// PGN Archival Service for storing game data on decentralized storage | ||||||||||||||||||||||||||||
| #[derive(Clone, Debug)] | ||||||||||||||||||||||||||||
| pub struct PgnArchiveService { | ||||||||||||||||||||||||||||
| client: reqwest::Client, | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| impl PgnArchiveService { | ||||||||||||||||||||||||||||
| pub fn new() -> Self { | ||||||||||||||||||||||||||||
| PgnArchiveService { | ||||||||||||||||||||||||||||
| client: reqwest::Client::new(), | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+62
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add HTTP timeouts to the The default 🛠️ Suggested fix-impl PgnArchiveService {
- pub fn new() -> Self {
- PgnArchiveService {
- client: reqwest::Client::new(),
- }
- }
+impl PgnArchiveService {
+ pub fn new() -> Self {
+ let client = reqwest::Client::builder()
+ .connect_timeout(std::time::Duration::from_secs(5))
+ .timeout(std::time::Duration::from_secs(30))
+ .build()
+ .expect("failed to build reqwest client");
+ PgnArchiveService { client }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Archive PGN to IPFS | ||||||||||||||||||||||||||||
| pub async fn archive_to_ipfs(&self, pgn_content: &str) -> Result<String, PgnArchiveError> { | ||||||||||||||||||||||||||||
| let ipfs_url = env::var("IPFS_API_URL") | ||||||||||||||||||||||||||||
| .unwrap_or_else(|_| "https://api.pinata.cloud/pinning/pinJSONToIPFS".to_string()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let ipfs_api_key = env::var("IPFS_API_KEY") | ||||||||||||||||||||||||||||
| .unwrap_or_else(|_| "".to_string()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let ipfs_secret_key = env::var("IPFS_SECRET_KEY") | ||||||||||||||||||||||||||||
| .unwrap_or_else(|_| "".to_string()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let payload = serde_json::json!({ | ||||||||||||||||||||||||||||
| "pinataContent": { | ||||||||||||||||||||||||||||
| "pgn": pgn_content, | ||||||||||||||||||||||||||||
| "type": "chess_game", | ||||||||||||||||||||||||||||
| "archived_at": chrono::Utc::now().to_rfc3339() | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| "pinataMetadata": { | ||||||||||||||||||||||||||||
| "name": format!("xlmate_game_{}.pgn", chrono::Utc::now().timestamp()) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let response = self.client | ||||||||||||||||||||||||||||
| .post(&ipfs_url) | ||||||||||||||||||||||||||||
| .header("Content-Type", "application/json") | ||||||||||||||||||||||||||||
| .header("pinata_api_key", &ipfs_api_key) | ||||||||||||||||||||||||||||
| .header("pinata_secret_api_key", &ipfs_secret_key) | ||||||||||||||||||||||||||||
| .json(&payload) | ||||||||||||||||||||||||||||
| .send() | ||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||
| .map_err(|e| PgnArchiveError::NetworkError(e.to_string()))?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if !response.status().is_success() { | ||||||||||||||||||||||||||||
| return Err(PgnArchiveError::IpfsError( | ||||||||||||||||||||||||||||
| format!("IPFS upload failed with status: {}", response.status()) | ||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let ipfs_response: IpfsResponse = response | ||||||||||||||||||||||||||||
| .json() | ||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||
| .map_err(|e| PgnArchiveError::SerializationError(e.to_string()))?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Ok(ipfs_response.ipfs_hash) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Archive PGN to Arweave | ||||||||||||||||||||||||||||
| pub async fn archive_to_arweave(&self, pgn_content: &str) -> Result<String, PgnArchiveError> { | ||||||||||||||||||||||||||||
| let arweave_url = env::var("ARWEAVE_GATEWAY_URL") | ||||||||||||||||||||||||||||
| .unwrap_or_else(|_| "https://arweave.net".to_string()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // For simplicity, we're using a bundler service | ||||||||||||||||||||||||||||
| // In production, you'd use the Arweave SDK with a wallet | ||||||||||||||||||||||||||||
| let bundler_url = env::var("ARWEAVE_BUNDLER_URL") | ||||||||||||||||||||||||||||
| .unwrap_or_else(|_| "https://node2.bundlr.network".to_string()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Calculate SHA-256 hash of PGN for integrity | ||||||||||||||||||||||||||||
| let pgn_hash = self.calculate_hash(pgn_content); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let payload = serde_json::json!({ | ||||||||||||||||||||||||||||
| "data": pgn_content, | ||||||||||||||||||||||||||||
| "tags": [ | ||||||||||||||||||||||||||||
| { "name": "Content-Type", "value": "application/x-chess-pgn" }, | ||||||||||||||||||||||||||||
| { "name": "App", "value": "XLMate" }, | ||||||||||||||||||||||||||||
| { "name": "PGN-Hash", "value": &pgn_hash }, | ||||||||||||||||||||||||||||
| { "name": "Archived-At", "value": &chrono::Utc::now().to_rfc3339() } | ||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let response = self.client | ||||||||||||||||||||||||||||
| .post(&format!("{}/tx", bundler_url)) | ||||||||||||||||||||||||||||
| .header("Content-Type", "application/json") | ||||||||||||||||||||||||||||
| .json(&payload) | ||||||||||||||||||||||||||||
| .send() | ||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||
| .map_err(|e| PgnArchiveError::NetworkError(e.to_string()))?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if !response.status().is_success() { | ||||||||||||||||||||||||||||
| return Err(PgnArchiveError::ArweaveError( | ||||||||||||||||||||||||||||
| format!("Arweave upload failed with status: {}", response.status()) | ||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let arweave_response: ArweaveResponse = response | ||||||||||||||||||||||||||||
| .json() | ||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||
| .map_err(|e| PgnArchiveError::SerializationError(e.to_string()))?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Ok(arweave_response.id) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+115
to
+157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The /tx endpoint on Bundlr/Irys nodes (e.g., https://node1.irys.xyz/tx or https://upload.ardrive.io/v1/tx) accepts POST requests with a signed ANS-104 DataItem as the raw binary request body. Request format: - Method: POST - URL: /{token}/tx or /tx or /v1/tx (token-specific path optional) - Headers: Content-Type: application/octet-stream - Body: Raw bytes of the fully signed DataItem (binary format per ANS-104) The signed DataItem is a binary structure: - Signature type (2 bytes) - Signature (variable) - Owner (variable) - Target (32 bytes + presence byte, optional) - Anchor (32 bytes + presence byte, optional) - Number of tags (8 bytes) - Number of tag bytes (8 bytes) - Tags (binary Avro array) - Data (variable binary payload) To create: Use libraries like Citations:
🏁 Script executed: # Check the file exists and examine the lines in question
head -c 5000 backend/modules/api/src/pgn_archive.rs | tail -c 3000Repository: NOVUS-X/XLMate Length of output: 3058 🏁 Script executed: # Get more context around lines 115-157
sed -n '110,160p' backend/modules/api/src/pgn_archive.rsRepository: NOVUS-X/XLMate Length of output: 1924 🏁 Script executed: # Search for any signing-related code in the file
rg -n "sign|Sign|bundler|ANS-104|dataitem" backend/modules/api/src/pgn_archive.rsRepository: NOVUS-X/XLMate Length of output: 235 🏁 Script executed: # Find the archive_pgn method to see the fallback logic
sed -n '160,220p' backend/modules/api/src/pgn_archive.rsRepository: NOVUS-X/XLMate Length of output: 2062 🏁 Script executed: # Search for where archive_to_arweave is called and how errors are handled
rg -n "archive_to_arweave|Arweave" backend/modules/api/src/pgn_archive.rs -A 3Repository: NOVUS-X/XLMate Length of output: 2232 Arweave bundler request is unsigned — will 4xx in production. Bundlr/Irys Either:
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Archive PGN to both IPFS and Arweave | ||||||||||||||||||||||||||||
| pub async fn archive_pgn( | ||||||||||||||||||||||||||||
| &self, | ||||||||||||||||||||||||||||
| game_id: &str, | ||||||||||||||||||||||||||||
| pgn_content: &str, | ||||||||||||||||||||||||||||
| ) -> Result<PgnArchiveResult, PgnArchiveError> { | ||||||||||||||||||||||||||||
| let pgn_hash = self.calculate_hash(pgn_content); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Archive to both services in parallel | ||||||||||||||||||||||||||||
| let ipfs_future = self.archive_to_ipfs(pgn_content); | ||||||||||||||||||||||||||||
| let arweave_future = self.archive_to_arweave(pgn_content); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let (ipfs_result, arweave_result) = tokio::join!(ipfs_future, arweave_future); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let ipfs_cid = match ipfs_result { | ||||||||||||||||||||||||||||
| Ok(cid) => { | ||||||||||||||||||||||||||||
| log::info!("PGN archived to IPFS: {}", cid); | ||||||||||||||||||||||||||||
| Some(cid) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| Err(e) => { | ||||||||||||||||||||||||||||
| log::warn!("Failed to archive to IPFS: {}", e); | ||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let arweave_tx_id = match arweave_result { | ||||||||||||||||||||||||||||
| Ok(tx_id) => { | ||||||||||||||||||||||||||||
| log::info!("PGN archived to Arweave: {}", tx_id); | ||||||||||||||||||||||||||||
| Some(tx_id) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| Err(e) => { | ||||||||||||||||||||||||||||
| log::warn!("Failed to archive to Arweave: {}", e); | ||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if ipfs_cid.is_none() && arweave_tx_id.is_none() { | ||||||||||||||||||||||||||||
| return Err(PgnArchiveError::NetworkError( | ||||||||||||||||||||||||||||
| "Failed to archive PGN to any storage service".to_string() | ||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Ok(PgnArchiveResult { | ||||||||||||||||||||||||||||
| game_id: game_id.to_string(), | ||||||||||||||||||||||||||||
| ipfs_cid, | ||||||||||||||||||||||||||||
| arweave_tx_id, | ||||||||||||||||||||||||||||
| pgn_hash, | ||||||||||||||||||||||||||||
| archived_at: chrono::Utc::now().to_rfc3339(), | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Calculate SHA-256 hash of PGN content | ||||||||||||||||||||||||||||
| fn calculate_hash(&self, content: &str) -> String { | ||||||||||||||||||||||||||||
| use sha2::{Sha256, Digest}; | ||||||||||||||||||||||||||||
| let mut hasher = Sha256::new(); | ||||||||||||||||||||||||||||
| hasher.update(content.as_bytes()); | ||||||||||||||||||||||||||||
| hex::encode(hasher.finalize()) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /// Verify PGN integrity by comparing hash | ||||||||||||||||||||||||||||
| pub fn verify_pgn_integrity(&self, pgn_content: &str, expected_hash: &str) -> bool { | ||||||||||||||||||||||||||||
| let actual_hash = self.calculate_hash(pgn_content); | ||||||||||||||||||||||||||||
| actual_hash == expected_hash | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
actix-web ConnectionInfo real_ip_remote_addr spoofing X-Forwarded-For trusted proxy💡 Result:
actix-web ConnectionInfo does not provide a built-in “trusted proxy” allowlist; it will read client IP information from spoofable headers by default unless you control who can set them (e.g., by stripping/overwriting at a reverse proxy). 1) What ConnectionInfo uses for real_ip_remote_addr - ConnectionInfo::realip_remote_addr resolves the “real IP” (remote address of the client) in this order: - Forwarded header (RFC 7239) - X-Forwarded-For header - peer address from the opened socket (same as peer_addr) This behavior is defined in actix-web’s ConnectionInfo docs/source. 2) Spoofing risk (no trust model) - The docs explicitly warn: do not use realip_remote_addr for security purposes unless you can be sure the Forwarded and X-Forwarded-For headers cannot be spoofed by the client. If you’re running without a proxy, obtaining the peer address would be more appropriate. - actix-web also notes that Forwarded/X-Forwarded-* values expose privacy-sensitive info (like client IP) and must be considered when deploying. 3) “trusted proxy” guidance for X-Forwarded-For - Because actix-web’s ConnectionInfo simply parses these headers, your application needs a deployment-level strategy to ensure only your reverse proxy can influence them. Typical approach: - Configure your reverse proxy to overwrite X-Forwarded-For (and Forwarded if used) and to remove any incoming copies from the client. - Ensure the actix-web server is not directly reachable from untrusted networks (only through the reverse proxy / internal network). - Then, it is safe to treat realip_remote_addr as the proxy-supplied client IP for logging/diagnostics; for security decisions, still prefer using peer_addr or implement explicit trust logic yourself. 4) actix-web-related misconception: “trusted proxy” doesn’t exist in ConnectionInfo - There is an open discussion/issue specifically highlighting that realip_remote_addr behavior can lead to insecure code when apps use it unconditionally. 5) Practical options if you need explicit trust - actix-web itself: no built-in trusted-proxy config for ConnectionInfo. - If you need “trusted proxy” behavior, you generally implement it via: - middleware that extracts IP only when the request comes from known proxy IPs, or - custom header parsing with an allowlist of proxy addresses (not shown in actix-web ConnectionInfo docs). Example takeaway: If you want “X-Forwarded-For trusted proxy” semantics, you must enforce them outside ConnectionInfo (reverse proxy configuration + network isolation, or custom middleware/logic).
Citations:
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 793
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 405
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 742
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 1657
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 106
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 1671
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 808
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 84
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 11672
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 1004
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 513
🏁 Script executed:
Repository: NOVUS-X/XLMate
Length of output: 4085
Remove
real_ip_remote_addr()or validate it through a trusted proxy.ConnectionInfo::real_ip_remote_addr()reads the leftmostX-Forwarded-FororForwardedheader without validation. Any direct client can sendX-Forwarded-For: 8.8.8.8and poison the storedip_address. This is amplified by the incomplete IP binding feature—generate_token_with_session()currently passesip_hash: None, so the session IP is stored but never validated on refresh/validate, leaving spoofing undetected.Use
peer_addr()for direct deployments, or if behind a proxy, validate that the request'speer_addrmatches a configured trusted proxy CIDR before acceptingX-Forwarded-For. Also implement consistent IP validation across login, refresh, and validate endpoints (currently missing).🤖 Prompt for AI Agents