Repository navigation
Groups and selectors: raw cursors and npm audits name nothing hidden - #700
Merged
Merged
Conversation
A raw listing set a page's `next` to the last entry it walked, whether a content selector hid it or not, and a group passed the smallest member cursor on unchanged: following `after=` walked through every hidden name of a directory, in the JSON, the `Link` header and the HTML page alike. The same cursor told a caller that a directory of hidden files existed (a 301 for the directory probe, a 200 with `next` for a directory past a page). A page now reads on past hidden entries until it holds `limit` visible ones, walking at most 1,000 entries (`LISTING_SCAN`) for a caller with a selector, and `next` is the last visible one: at the bound the page ends there, and the listing ends when it found none. A directory exists for the caller only when it holds a visible entry. A caller without a selector walks exactly what it did before. `npm audit` through a group (or straight to a proxy) left out only the reserved prefixes on the way, so a package a caller's selector hid still went to the proxy's upstream with the proxy's credentials. A forward now also leaves out every package the caller's package filter hides on any repository on the way to the proxy, the proxy's own included, and refuses (400) a body it cannot filter. Tests cover a group of two hosted members for NuGet, npm, raw and Docker, read by a caller who may read only one member, one with a selector on the group's grant and one with a selector on a member's: downloads and metadata, search and listings with their counts, the browse API, raw cursors and directories (also of a hosted and a proxy repository), and npm audits through nested groups. Docker refuses a name outside a selector of the requested repository's grant by name (403, its registry token grants no `pull` on it), so a hidden image answers as an absent one of the same name pattern does, which the test checks rather than a 404. Fixes #648 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e6DJZaXD2mBH9tURsdutM
This was referenced Oct 30, 2026
Docker: an image a selector of the caller's own grant hides answers 404 (NAME_UNKNOWN), not 403
#703
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #648
Raw:
nextand the directory probe named hidden entrieslist_localset a page's cursor to the last entry it walked, hidden or not, and a group passed the smallest member cursor on unchanged. With a selectorpublic/**,GET /repository/<group>/?limit=1&format=jsonanswered{"entries":[],"next":"internal-a/"}(the new test reproduces exactly that onmain), and followingafter=walked every hidden name. The same cursor made a directory of hidden files answer 301 (probe withlimit: 1) or 200 with anext.Now (
crates/server/src/files/listing.rs):limitvisible ones (plus one, to know whether another page follows), andnextis the last visible entry;LISTING_SCAN= 1,000 entries per page, in batches of at least 100. A page that reaches the bound ends at its last visible entry; with none, the listing ends there (documented indocs/raw.md: visible entries behind more than 1,000 hidden ones in a row are not listed);/, whatever its size;limit + 1entries, as before.The group merge is unchanged: each member's
morenow names a visible entry, so the group's cutoff does too.npm: an audit through a group ignored selectors
forward_sourcenow collects, besides the reserved prefixes, the caller's package filter on every repository on the way to the proxy (the requested group, nested groups and the proxy itself). The body is filtered when any of them restricts (Withheld::hides); a body that cannot be filtered is refused with 400 as before. A proxy audited directly with a selector on its grant is filtered too.Tests
common::TestServer::group_callerssets up the issue's three callers over a group of two hosted members (a reader of one member; a selector on the group's grant; a selector on a member's grant), each with what it should see. With it:browse.rs): flat container,.nupkg, registration,/browse/packages/{repo}/{id}404; search and browse list only what's visible,totalHits/totalincluded.npm.rs): packument (both forms), tarball, dist-tags, browse page 404; search and browse lists with counts. A second test audits (bulk and legacy) through nested groups with the selector on the outer group's, the inner group's or the proxy's grant, and directly on the proxy.raw.rs): files and.sha256; root andpublic/walked a page of one at a time, checking that the JSON, theLinkheader and the HTML page never contain a hidden name; hidden directories 404 (with and without/,?limit=1). Also a hosted and a proxy repository with a selector.registry.rs): manifest by tag and digest, blob, tag list, referrers; image page, chart page, image list withtotal.Old code: the raw test fails on the leaked
nextand the audit test onprivate-libreaching the upstream.One deviation from the acceptance criteria: Docker answers 403, not 404, under a selector on the group's own grant
Docker decides a name outside a selector of the requested repository's grant by name: the token service grants no
pullon it (docs/docker.md: "A registry token grants only what the caller may do when it is issued … content selectors match image names"), and the request is refused with 403DENIED, whether the image exists or not. So nothing leaks, but it is not a 404. The test asserts that a hidden image answers exactly like an absent image of the same name pattern (<group>/secret/none): 403 in that case, 404 for the other two. Turning it into a 404 would mean issuingpulltokens for names the caller may not read, so I left it. Say if you want it changed.Checks run
cargo test -p forge-server --lib --test raw --test npm --test registry --test browse: 247 + 9 + 16 + 45 + 11 passedcargo clippy -p forge-server -p forge-plugin-api --tests -- -D warnings,cargo fmt --all,cargo xtask changelog --checkChangelog:
docs/changes/2.2.0/648.security.md.Generated by Claude Code