Marketplace: validate tree entry names and cap depth/manifest size

Fix round 1 from PR review of the tree/catalog parsing:

- collect_dir now rejects an entry whose name is ".", "..", empty, or
  contains "/", "\" or NUL before it becomes part of an item's rel_path —
  a crafted git tree could otherwise walk a file outside the item's own
  folder once that path is joined against the item root downstream.
- collect_dir caps recursion at 32 directory levels and counts
  directories (not just files) toward MAX_ITEM_FILES, so a tree that is
  wide or deep rather than merely file-heavy is still bounded.
- hook.json and plugins/.claude-plugin/marketplace.json are now rejected
  unparsed above 1 MiB, rather than handed to serde_json regardless of
  size.

A pre-read size query (checking a blob's size before reading it) is
deferred per controller ruling — this round reads the blob and checks
its length before parsing, which is enough for the JSON-parsing DoS
shape being closed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 08:55:32 -07:00
co-authored by Claude Opus 5.5
parent 2e62728b06
commit b09f811ac1
2 changed files with 181 additions and 5 deletions
+23
View File
@@ -92,6 +92,29 @@ impl MemTree {
self
}
/// Place a file so that, inside `dir`, it is listed under the literal
/// entry name `name` — including a name `file`/`exec_file`/`symlink`
/// could never be asked to produce because it doesn't correspond to any
/// real filesystem path a caller here would construct: `.`, `..`, empty,
/// or containing `/`, `\` or a NUL byte. Exists only so a test can drive
/// `catalog::collect_dir`'s hostile-entry-name rejection without relying
/// on incidental behaviour of path-string splitting.
pub fn raw_named_file(mut self, dir: &str, name: &str, contents: &str) -> Self {
let path = if dir.is_empty() {
name.to_string()
} else {
format!("{}/{}", dir, name)
};
self.nodes.insert(
path,
MemNode::File {
data: contents.as_bytes().to_vec(),
executable: false,
},
);
self
}
fn is_dir(&self, path: &str) -> bool {
if path.is_empty() {
return true;