From c77743379b93a7efecef832610e5ba959bf7be49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:34:10 +0000 Subject: [PATCH 1/4] Initial plan From 3c3c96ddd8c995b427c3f31aa6a2b51721014738 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:38:52 +0000 Subject: [PATCH 2/4] Implement URL normalization for Referer header validation to prevent security bypasses Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- server/src/handlers/project.rs | 75 +++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/server/src/handlers/project.rs b/server/src/handlers/project.rs index 83092c9..daf5fe4 100644 --- a/server/src/handlers/project.rs +++ b/server/src/handlers/project.rs @@ -17,12 +17,33 @@ use futures::StreamExt; use crate::state::{AppState, SessionContext}; use crate::models::{User, ProjectSummary, PublishRequest, WhitelistRequest}; use std::collections::HashMap; +use url::Url; #[derive(Deserialize)] pub struct SearchQuery { q: String, } +/// Normalizes a URL by extracting scheme + host + path (without query params or fragments). +/// This prevents bypass attempts using query parameters or fragments. +/// Returns None if the URL cannot be parsed. +fn normalize_url(url_str: &str) -> Option { + Url::parse(url_str).ok().map(|url| { + let scheme = url.scheme(); + let host = url.host_str().unwrap_or(""); + let path = url.path(); + + // Normalize trailing slashes for consistency + let normalized_path = if path == "/" || path.is_empty() { + "/" + } else { + path.trim_end_matches('/') + }; + + format!("{}://{}{}", scheme, host, normalized_path) + }) +} + pub fn routes() -> Router { Router::new() .route("/api/my-projects", get(list_user_projects)) @@ -280,24 +301,32 @@ pub async fn get_project( // Optional boolean, safely unwrapped later let whitelist_allowed: Option = if let Some(referer_url) = referer { - let exists_row: Option<(bool,)> = sqlx::query_as( - "SELECT TRUE FROM project_whitelists \ - WHERE project_id = $1 AND allowed_url = $2 \ - LIMIT 1", - ) - .bind(project_id) - .bind(&referer_url) - .fetch_optional(&state.db) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Whitelist DB Error: {}", e), + // Normalize the Referer URL to prevent bypasses via query params or fragments + let normalized_referer = normalize_url(&referer_url); + + if let Some(normalized) = normalized_referer { + let exists_row: Option<(bool,)> = sqlx::query_as( + "SELECT TRUE FROM project_whitelists \ + WHERE project_id = $1 AND allowed_url = $2 \ + LIMIT 1", ) - })?; - - // TRUE row exists => allowed; otherwise false - Some(exists_row.is_some()) + .bind(project_id) + .bind(&normalized) + .fetch_optional(&state.db) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Whitelist DB Error: {}", e), + ) + })?; + + // TRUE row exists => allowed; otherwise false + Some(exists_row.is_some()) + } else { + // If URL parsing fails, deny access + Some(false) + } } else { None }; @@ -525,6 +554,10 @@ pub async fn add_to_whitelist( None => return Err((StatusCode::NOT_FOUND, "Project not found".to_string())), }; + // Normalize the URL to prevent bypasses via query params or fragments + let normalized_url = normalize_url(trimmed_url) + .ok_or((StatusCode::BAD_REQUEST, "Invalid URL format".to_string()))?; + // Unique(project_id, allowed_url) is enforced by the DB; ignore conflicts let result = sqlx::query( "INSERT INTO project_whitelists (project_id, allowed_url) \ @@ -532,7 +565,7 @@ pub async fn add_to_whitelist( ON CONFLICT (project_id, allowed_url) DO NOTHING", ) .bind(project_id) - .bind(trimmed_url) + .bind(&normalized_url) .execute(&state.db) .await; @@ -561,6 +594,10 @@ pub async fn remove_from_whitelist( return Err((StatusCode::BAD_REQUEST, "allowed_url is required".to_string())); } + // Normalize the URL to match how it was stored + let normalized_url = normalize_url(trimmed_url) + .ok_or((StatusCode::BAD_REQUEST, "Invalid URL format".to_string()))?; + let delete_result = sqlx::query( "DELETE FROM project_whitelists pw \ USING projects p \ @@ -571,7 +608,7 @@ pub async fn remove_from_whitelist( ) .bind(user.id) .bind(&slug) - .bind(trimmed_url) + .bind(&normalized_url) .execute(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB Error: {}", e)))?; From 1f36148a91767a727f53bc8f44e14d22e371ce7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:40:25 +0000 Subject: [PATCH 3/4] Enhance URL normalization security: restrict to http/https, require valid hosts Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- server/src/handlers/project.rs | 49 +++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/server/src/handlers/project.rs b/server/src/handlers/project.rs index daf5fe4..69533bf 100644 --- a/server/src/handlers/project.rs +++ b/server/src/handlers/project.rs @@ -25,23 +25,40 @@ pub struct SearchQuery { } /// Normalizes a URL by extracting scheme + host + path (without query params or fragments). -/// This prevents bypass attempts using query parameters or fragments. -/// Returns None if the URL cannot be parsed. +/// +/// This prevents bypass attempts using query parameters or fragments. For example: +/// - `https://example.com/path?bypass=1` -> `https://example.com/path` +/// - `https://example.com/path#fragment` -> `https://example.com/path` +/// - `https://example.com/path/` -> `https://example.com/path` +/// +/// # Security +/// Only http and https URLs with valid hosts are accepted. URLs without hosts or +/// with other schemes are rejected to prevent security issues. +/// +/// # Returns +/// - `Some(normalized_url)` if the URL is valid and has http/https scheme with a host +/// - `None` if the URL cannot be parsed, lacks a host, or uses a non-http(s) scheme fn normalize_url(url_str: &str) -> Option { - Url::parse(url_str).ok().map(|url| { - let scheme = url.scheme(); - let host = url.host_str().unwrap_or(""); - let path = url.path(); - - // Normalize trailing slashes for consistency - let normalized_path = if path == "/" || path.is_empty() { - "/" - } else { - path.trim_end_matches('/') - }; - - format!("{}://{}{}", scheme, host, normalized_path) - }) + let url = Url::parse(url_str).ok()?; + + // Only accept http and https schemes for security + let scheme = url.scheme(); + if scheme != "http" && scheme != "https" { + return None; + } + + // Require a valid host for http(s) URLs + let host = url.host_str()?; + let path = url.path(); + + // Normalize trailing slashes for consistency + let normalized_path = if path == "/" || path.is_empty() { + "/" + } else { + path.trim_end_matches('/') + }; + + Some(format!("{}://{}{}", scheme, host, normalized_path)) } pub fn routes() -> Router { From ad406be9b18e65f81f5e01889b2e19fc1cd7531b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:44:25 +0000 Subject: [PATCH 4/4] Add logging and improve error messages for URL validation Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- server/src/handlers/project.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/server/src/handlers/project.rs b/server/src/handlers/project.rs index 69533bf..7e38965 100644 --- a/server/src/handlers/project.rs +++ b/server/src/handlers/project.rs @@ -341,7 +341,8 @@ pub async fn get_project( // TRUE row exists => allowed; otherwise false Some(exists_row.is_some()) } else { - // If URL parsing fails, deny access + // If URL parsing fails, deny access and log for security monitoring + tracing::warn!("Referer normalization failed for project {}: {}", project_id, referer_url); Some(false) } } else { @@ -572,8 +573,10 @@ pub async fn add_to_whitelist( }; // Normalize the URL to prevent bypasses via query params or fragments - let normalized_url = normalize_url(trimmed_url) - .ok_or((StatusCode::BAD_REQUEST, "Invalid URL format".to_string()))?; + let normalized_url = normalize_url(trimmed_url).ok_or(( + StatusCode::BAD_REQUEST, + "Invalid URL format. URL must use http or https scheme and include a valid host.".to_string(), + ))?; // Unique(project_id, allowed_url) is enforced by the DB; ignore conflicts let result = sqlx::query( @@ -612,8 +615,10 @@ pub async fn remove_from_whitelist( } // Normalize the URL to match how it was stored - let normalized_url = normalize_url(trimmed_url) - .ok_or((StatusCode::BAD_REQUEST, "Invalid URL format".to_string()))?; + let normalized_url = normalize_url(trimmed_url).ok_or(( + StatusCode::BAD_REQUEST, + "Invalid URL format. URL must use http or https scheme and include a valid host.".to_string(), + ))?; let delete_result = sqlx::query( "DELETE FROM project_whitelists pw \