refactor(server): fold in PM review notes on classify_path

- classify_path now Rejects a collection slug containing '.' (mirrors
  OrgCollections::validate, plan L317, and item_path's documented contract,
  plan L990). Unreachable today since git normalizes './' away, but keeps the
  pre-receive hook self-defensive against path traversal.
- Rename test item_write_nested_slug_takes_leading_segment_only ->
  item_write_nested_slug_is_rejected (it asserts Rejected; old name misled).
- Add dotted_slug_is_rejected covering the new guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5brcDrT35r5GaJySXD5ja
This commit is contained in:
adlee-was-taken
2026-06-19 23:06:48 -04:00
parent 675b7836e1
commit 2dd5d79f36
2 changed files with 19 additions and 2 deletions

View File

@@ -32,6 +32,13 @@ pub fn classify_path(path: &str) -> PathClass {
if slug.is_empty() { if slug.is_empty() {
return PathClass::Rejected("empty collection slug in items path".to_string()); return PathClass::Rejected("empty collection slug in items path".to_string());
} }
// Defense-in-depth: mirror `OrgCollections::validate` — a slug containing
// '.' (e.g. a `..`/`.` path-traversal attempt) is structurally invalid.
// git normalizes most `./` away before the hook sees the path, so this is
// unreachable today; it keeps the hook self-defensive regardless.
if slug.contains('.') {
return PathClass::Rejected(format!("invalid collection slug: {:?}", slug));
}
return PathClass::Item { collection: slug.to_string() }; return PathClass::Item { collection: slug.to_string() };
} }

View File

@@ -18,8 +18,8 @@ fn item_write_yields_collection_slug() {
} }
#[test] #[test]
fn item_write_nested_slug_takes_leading_segment_only() { fn item_write_nested_slug_is_rejected() {
// Slugs cannot contain '/', so a 4-segment path is malformed → Rejected. // Slugs cannot contain '/', so a path with extra segments is malformed → Rejected.
assert_eq!( assert_eq!(
classify_path("items/prod/sub/x.enc"), classify_path("items/prod/sub/x.enc"),
PathClass::Rejected("items path must be items/<slug>/<id>.enc".to_string()) PathClass::Rejected("items path must be items/<slug>/<id>.enc".to_string())
@@ -51,6 +51,16 @@ fn empty_slug_segment_is_rejected() {
); );
} }
#[test]
fn dotted_slug_is_rejected() {
// Defense-in-depth (mirrors OrgCollections::validate): a slug containing '.'
// — e.g. a ".."/"." path-traversal attempt — is rejected.
assert_eq!(
classify_path("items/../x.enc"),
PathClass::Rejected("invalid collection slug: \"..\"".to_string())
);
}
use relicario_server::extract_schema_version; use relicario_server::extract_schema_version;
#[test] #[test]