diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-04-09 15:24:17 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-04-09 15:24:17 +0000 |
| commit | 2d74b9ca69b3a1e0b9a2359c12cc2d1979fc6130 (patch) | |
| tree | 61180841310feaca54c1661552d88347a0bebd72 /src/http/mod.rs | |
| parent | 28168a7701c897a5b6af13bc472d6f5902e0a96d (diff) | |
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.
Diffstat (limited to 'src/http/mod.rs')
| -rw-r--r-- | src/http/mod.rs | 19 |
1 files changed, 10 insertions, 9 deletions
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"); | |||
| 42 | /// | 42 | /// |
| 43 | /// Parses paths like `/<npub>/<identifier>.git` (for repository webpage/404) | 43 | /// Parses paths like `/<npub>/<identifier>.git` (for repository webpage/404) |
| 44 | /// | 44 | /// |
| 45 | /// The identifier is percent-decoded so that URLs like `/npub1.../my%20repo.git` | ||
| 46 | /// resolve to the correct filesystem path. | ||
| 47 | /// | ||
| 45 | /// Returns (npub, identifier) if the path matches a repository URL pattern | 48 | /// Returns (npub, identifier) if the path matches a repository URL pattern |
| 46 | fn parse_repo_url(path: &str) -> Option<(&str, &str)> { | 49 | fn parse_repo_url(path: &str) -> Option<(String, String)> { |
| 47 | // Remove leading slash | 50 | // Remove leading slash |
| 48 | let path = path.strip_prefix('/').unwrap_or(path); | 51 | let path = path.strip_prefix('/').unwrap_or(path); |
| 49 | 52 | ||
| @@ -56,7 +59,7 @@ fn parse_repo_url(path: &str) -> Option<(&str, &str)> { | |||
| 56 | } | 59 | } |
| 57 | 60 | ||
| 58 | let npub = parts[0]; | 61 | let npub = parts[0]; |
| 59 | let repo_part = parts[1]; | 62 | let repo_part = git::percent_decode(parts[1]); |
| 60 | 63 | ||
| 61 | // The repo part must end with .git | 64 | // The repo part must end with .git |
| 62 | if !repo_part.ends_with(".git") { | 65 | if !repo_part.ends_with(".git") { |
| @@ -69,14 +72,17 @@ fn parse_repo_url(path: &str) -> Option<(&str, &str)> { | |||
| 69 | } | 72 | } |
| 70 | 73 | ||
| 71 | // Extract identifier (remove .git suffix) | 74 | // Extract identifier (remove .git suffix) |
| 72 | let identifier = repo_part.strip_suffix(".git").unwrap_or(repo_part); | 75 | let identifier = repo_part |
| 76 | .strip_suffix(".git") | ||
| 77 | .unwrap_or(&repo_part) | ||
| 78 | .to_string(); | ||
| 73 | 79 | ||
| 74 | // Identifier must not be empty | 80 | // Identifier must not be empty |
| 75 | if identifier.is_empty() { | 81 | if identifier.is_empty() { |
| 76 | return None; | 82 | return None; |
| 77 | } | 83 | } |
| 78 | 84 | ||
| 79 | Some((npub, identifier)) | 85 | Some((npub.to_string(), identifier)) |
| 80 | } | 86 | } |
| 81 | 87 | ||
| 82 | /// Add CORS headers to a response builder | 88 | /// Add CORS headers to a response builder |
| @@ -160,9 +166,6 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 160 | 166 | ||
| 161 | // Check for Git HTTP requests first | 167 | // Check for Git HTTP requests first |
| 162 | if let Some((npub, identifier, subpath)) = git::parse_git_url(&path) { | 168 | if let Some((npub, identifier, subpath)) = git::parse_git_url(&path) { |
| 163 | let npub = npub.to_string(); | ||
| 164 | let identifier = identifier.to_string(); | ||
| 165 | let subpath = subpath.to_string(); | ||
| 166 | 169 | ||
| 167 | // Extract Git-Protocol header for protocol v2 support | 170 | // Extract Git-Protocol header for protocol v2 support |
| 168 | let git_protocol = req | 171 | let git_protocol = req |
| @@ -391,8 +394,6 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 391 | // GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) | 394 | // GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) |
| 392 | // to browse the repository and a 404 page for repositories it doesn't host" | 395 | // to browse the repository and a 404 page for repositories it doesn't host" |
| 393 | if let Some((npub, identifier)) = parse_repo_url(&path) { | 396 | if let Some((npub, identifier)) = parse_repo_url(&path) { |
| 394 | let npub = npub.to_string(); | ||
| 395 | let identifier = identifier.to_string(); | ||
| 396 | let config = self.config.clone(); | 397 | let config = self.config.clone(); |
| 397 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); | 398 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); |
| 398 | 399 | ||