You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #62. PR2 (#165) bounded the upload-pack/receive-pack POST via a timeout in run_git_service, and deliberately left the sibling git paths unbounded. Verifying that scope boundary turned up a genuine, anonymously reachable resource-exhaustion path worth closing on its own.
The bug
On a public repo that carries a path-scoped visibility rule, an anonymousPOST /:owner/:repo.git/git-upload-pack takes the filtered-pack branch and runs two unbounded, no-timeout git walks on tokio's blocking pool:
withheld_blob_oids (crates/gitlawb-node/src/api/repos.rs:629, inside spawn_blocking) -> visibility_pack::blob_paths: git rev-list --all, then one git ls-tree -rz <commit> per reachable commit.
Neither walk has a timeout, and the git-read routes have no per-IP rate limit: git_write_routes carries rate_limit_by_ip (crates/gitlawb-node/src/server.rs:196) but git_read_routes (server.rs:384-412) carries only body limits and optional_signature. The process runs the default #[tokio::main] runtime (crates/gitlawb-node/src/main.rs:41, no max_blocking_threads override anywhere), so the blocking pool is tokio's default 512 threads with an unbounded queue.
Reachability
git_upload_pack gates on visibility_check(rules, is_public, owner, caller, "/"). For a public repo an anonymous caller (caller = None) clears the "/" gate: a subtree rule such as /secret/** does not match "/" and the public fallback allows. has_path_scoped_rule(&rules) is then true, so control enters the filtered branch. No auth, no rate limit, no timeout.
Impact
Each request pins a blocking thread for the full ref walk plus pack build, with no deadline to shed it. Concurrent anonymous requests (bounded only by the 512-thread pool) occupy the pool; further spawn_blocking work queues without bound and the tasks awaiting it hang. Per-request cost scales with commit count and tree size, so a public repo with deep history under a path-scoped rule is an amplifier. This is an availability break reachable without credentials.
What is not the bug (checked)
info_refs (smart_http.rs:19) also lacks a timeout, but it runs git --advertise-refs via tokio::process::Command::output().await (async; it does not pin a worker thread), is bounded by ref count on a valid repo, and has no attacker-controllable cost knob short of a repo-corruption state an anonymous caller cannot induce on someone else's repo. It shares the missing-timeout shape but is not the exhaustion vector. Noting it only so a fix considers it, not as the finding.
Suggested direction
Put the git-read routes behind the same per-IP rate limiter as the write routes (rate_limit_by_ip), or a dedicated read limiter.
Bound the filtered pack build: wrap the spawn_blocking in a wall-clock deadline and abort the JoinHandle on expiry, returning 504, mirroring the run_git_service timeout from PR2. A tokio::time::timeout cannot cancel spawn_blocking, but the handle can be aborted and the request failed.
Follow-up to #62. PR2 (#165) bounded the upload-pack/receive-pack POST via a timeout in
run_git_service, and deliberately left the sibling git paths unbounded. Verifying that scope boundary turned up a genuine, anonymously reachable resource-exhaustion path worth closing on its own.The bug
On a public repo that carries a path-scoped visibility rule, an anonymous
POST /:owner/:repo.git/git-upload-packtakes the filtered-pack branch and runs two unbounded, no-timeout git walks on tokio's blocking pool:withheld_blob_oids(crates/gitlawb-node/src/api/repos.rs:629, insidespawn_blocking) ->visibility_pack::blob_paths:git rev-list --all, then onegit ls-tree -rz <commit>per reachable commit.smart_http::upload_pack_excluding(crates/gitlawb-node/src/api/repos.rs:647) ->build_filtered_pack(crates/gitlawb-node/src/git/smart_http.rs:288, insidespawn_blocking):git rev-list --objects --all+git pack-objects.Neither walk has a timeout, and the git-read routes have no per-IP rate limit:
git_write_routescarriesrate_limit_by_ip(crates/gitlawb-node/src/server.rs:196) butgit_read_routes(server.rs:384-412) carries only body limits andoptional_signature. The process runs the default#[tokio::main]runtime (crates/gitlawb-node/src/main.rs:41, nomax_blocking_threadsoverride anywhere), so the blocking pool is tokio's default 512 threads with an unbounded queue.Reachability
git_upload_packgates onvisibility_check(rules, is_public, owner, caller, "/"). For a public repo an anonymous caller (caller = None) clears the"/"gate: a subtree rule such as/secret/**does not match"/"and the public fallback allows.has_path_scoped_rule(&rules)is then true, so control enters the filtered branch. No auth, no rate limit, no timeout.Impact
Each request pins a blocking thread for the full ref walk plus pack build, with no deadline to shed it. Concurrent anonymous requests (bounded only by the 512-thread pool) occupy the pool; further
spawn_blockingwork queues without bound and the tasks awaiting it hang. Per-request cost scales with commit count and tree size, so a public repo with deep history under a path-scoped rule is an amplifier. This is an availability break reachable without credentials.What is not the bug (checked)
info_refs(smart_http.rs:19) also lacks a timeout, but it runsgit --advertise-refsviatokio::process::Command::output().await(async; it does not pin a worker thread), is bounded by ref count on a valid repo, and has no attacker-controllable cost knob short of a repo-corruption state an anonymous caller cannot induce on someone else's repo. It shares the missing-timeout shape but is not the exhaustion vector. Noting it only so a fix considers it, not as the finding.Suggested direction
rate_limit_by_ip), or a dedicated read limiter.spawn_blockingin a wall-clock deadline and abort theJoinHandleon expiry, returning 504, mirroring therun_git_servicetimeout from PR2. Atokio::time::timeoutcannot cancelspawn_blocking, but the handle can be aborted and the request failed.Verified against current
main. Refs #62, #165.