Skip to content

GET /ipfs/{cid} serves tree/commit objects of withheld subtrees, leaking structure get_tree protects (KTD3) #135

Description

@beardthelion

Summary

GET /ipfs/{cid} (get_by_cid) serves tree, commit, and tag objects unconditionally under path-scoped visibility rules, disclosing the structure of a withheld subtree (every child filename and child object id) to a caller who is denied that subtree. Blob content stays protected; this is a metadata/structure leak, not a content leak.

This is the KTD3 behavior that #126 explicitly scoped out, surfaced during review of #133 (which correctly fixes the blob-content half of the same endpoint). Filing so the divergence is tracked rather than buried in a PR thread.

Where

crates/gitlawb-node/src/api/ipfs.rs (the per-object gate):

let path_scoped = has_path_scoped_rule(rules);
if path_scoped && obj_type == "blob" {
    // ... allowed-set membership check; non-blobs skip it
}

Under a path-scoped rule the allowed-set gate applies only to obj_type == "blob". A tree/commit/tag object falls through and is served. A git tree object's raw body is <mode> <filename>\0<raw-oid> per entry, so fetching the tree CID of a withheld subtree returns every child filename and child oid in cleartext, recursively. The leaf blobs themselves still require allowed-set membership, so their content is protected.

Why it matters

This is the same confidentiality boundary that get_tree (crates/gitlawb-node/src/api/repos.rs:444, the N3 fix) deliberately defends: it gates on the requested subtree path precisely so "a caller denied a withheld subtree can still enumerate its names/SHAs" cannot happen. The two read surfaces over the same git data disagree: the REST tree-read path protects subtree names/oids, the IPFS CID path does not.

Bounded by the attacker needing the tree object's sha256 CID to request it. That is not free, but CID-enumeration surfaces exist (see #121), so it should not be relied on as the only barrier.

Scope / not a regression

Pre-existing. The pre-#133 deny-set (withheld_blob_oids) also enumerated only blob oids, so trees were never withheld there either. #133 preserves the behavior (and the merged test at test_support.rs asserts "tree object is served to anon (KTD3)"). This issue is about closing the divergence, not about #133.

Suggested direction

Gate non-blob objects under path-scoped rules against their own reachable path the way get_tree does, or make a deliberate, documented decision that tree/commit structure is non-confidential and reconcile get_tree to match. Either way the two surfaces should agree. A handler test should pin the chosen behavior by asserting the served body contents, not just the status code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:nodegitlawb-node — the serving node and REST APIkind:securityVulnerability fix or hardeningsev:mediumDegraded but workaround existssubsystem:apiNode REST API request/response surfacesubsystem:visibilityPath-scoped visibility and content withholding

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions