From 2d74b9ca69b3a1e0b9a2359c12cc2d1979fc6130 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 9 Apr 2026 15:24:17 +0000 Subject: fix: reject identifiers with whitespace and URL-decode path components Two bugs allowed a repository announcement with a space-containing identifier ('kuboslopp by Shakespeare') to enter purgatory and create a bare repo on disk, but then fail to serve git data over HTTP. Bug 1 (serving): parse_git_url and parse_repo_url did not percent-decode the URL path before resolving the filesystem path. A client requesting /npub.../kuboslopp%20by%20Shakespeare.git/info/refs had the identifier extracted as 'kuboslopp%20by%20Shakespeare' (literal %20), which did not match the on-disk directory 'kuboslopp by Shakespeare.git'. Fix: add percent_decode() in src/git/mod.rs and apply it to the repo component in both parse_git_url and parse_repo_url. Bug 2 (validation): validate_announcement did not check that the identifier is safe as a filesystem path component and URL segment. Identifiers containing whitespace, path separators, null bytes, or reserved names (. / ..) should be rejected at acceptance time. Fix: add validate_identifier() in src/nostr/events.rs and call it from validate_announcement before any other policy checks. --- src/http/mod.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'src/http') diff --git a/src/http/mod.rs b/src/http/mod.rs index c397365..154d6c5 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -42,8 +42,11 @@ const ICON_PNG: &[u8] = include_bytes!("../../static/icon.png"); /// /// Parses paths like `//.git` (for repository webpage/404) /// +/// The identifier is percent-decoded so that URLs like `/npub1.../my%20repo.git` +/// resolve to the correct filesystem path. +/// /// Returns (npub, identifier) if the path matches a repository URL pattern -fn parse_repo_url(path: &str) -> Option<(&str, &str)> { +fn parse_repo_url(path: &str) -> Option<(String, String)> { // Remove leading slash let path = path.strip_prefix('/').unwrap_or(path); @@ -56,7 +59,7 @@ fn parse_repo_url(path: &str) -> Option<(&str, &str)> { } let npub = parts[0]; - let repo_part = parts[1]; + let repo_part = git::percent_decode(parts[1]); // The repo part must end with .git if !repo_part.ends_with(".git") { @@ -69,14 +72,17 @@ fn parse_repo_url(path: &str) -> Option<(&str, &str)> { } // Extract identifier (remove .git suffix) - let identifier = repo_part.strip_suffix(".git").unwrap_or(repo_part); + let identifier = repo_part + .strip_suffix(".git") + .unwrap_or(&repo_part) + .to_string(); // Identifier must not be empty if identifier.is_empty() { return None; } - Some((npub, identifier)) + Some((npub.to_string(), identifier)) } /// Add CORS headers to a response builder @@ -160,9 +166,6 @@ impl Service> for HttpService { // Check for Git HTTP requests first if let Some((npub, identifier, subpath)) = git::parse_git_url(&path) { - let npub = npub.to_string(); - let identifier = identifier.to_string(); - let subpath = subpath.to_string(); // Extract Git-Protocol header for protocol v2 support let git_protocol = req @@ -391,8 +394,6 @@ impl Service> for HttpService { // GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) // to browse the repository and a 404 page for repositories it doesn't host" if let Some((npub, identifier)) = parse_repo_url(&path) { - let npub = npub.to_string(); - let identifier = identifier.to_string(); let config = self.config.clone(); let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); -- cgit v1.2.3