diff --git a/crates/gitlawb-node/src/api/visibility.rs b/crates/gitlawb-node/src/api/visibility.rs index 6665b9e8..ba94fe9f 100644 --- a/crates/gitlawb-node/src/api/visibility.rs +++ b/crates/gitlawb-node/src/api/visibility.rs @@ -57,9 +57,23 @@ fn validate_path_glob(path_glob: &str) -> Result<()> { "path_glob must not end with '/'".into(), )); } - if path_glob.contains('*') && !path_glob.ends_with("/**") { + // Reject empty path segments. `glob_prefix` collapses "//**" to "/" (whole + // repo), so without this an operator could bypass the "/**" guard above and + // grant repo-wide read with "//**", or set a dead rule like "//secret/**" + // whose prefix never matches a real single-slash path and silently leaves + // the subtree readable. + if path_glob.contains("//") { return Err(AppError::BadRequest( - "the only supported wildcard is a trailing '/**'".into(), + "path_glob must not contain empty path segments".into(), + )); + } + // Strip the one permitted trailing "/**" before checking for stray + // wildcards, otherwise a "*" elsewhere in the prefix (e.g. "/a*b/**" or + // "/a/**/**") slips through because the string still ends with "/**". + let prefix = path_glob.strip_suffix("/**").unwrap_or(path_glob); + if prefix.contains('*') { + return Err(AppError::BadRequest( + "the only supported wildcard is a single trailing '/**'".into(), )); } Ok(()) @@ -240,9 +254,22 @@ mod tests { #[test] fn rejects_malformed_globs() { - // empty, no leading slash, whole-repo via "/**", trailing slash, and - // non-trailing wildcards are all rejected. - for g in ["", "secret/**", "/**", "/secret/", "/a*b", "/*/x"] { + // empty, no leading slash, whole-repo via "/**", trailing slash, + // non-trailing wildcards, double wildcards, and empty path segments + // (which would collapse to whole-repo or a dead rule) are all rejected. + for g in [ + "", + "secret/**", + "/**", + "/**/**", + "/secret/", + "/a*b", + "/*/x", + "/a*b/**", + "/a/**/**", + "//**", + "//secret/**", + ] { assert!(validate_path_glob(g).is_err(), "{g} should be rejected"); } }