Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

#48

Bound the anonymous Git HTTP read path

merged · by keenan 13d ago · merged into main at 13cff33f by keenan 13d ago

keenan opened this 13d ago

Closes #4.

Why

server/githttp served anonymous upload-pack with no cap on the request body (it refused a negative Content-Length and accepted anything else), ran git http-backend with no request context or deadline, refused only paths that did not start with git-upload-pack — passing everything after that prefix to the CGI as PATH_INFO — and server/server.go set ReadHeaderTimeout and IdleTimeout but no ReadTimeout and no MaxHeaderBytes.

Read-only is not cheap. Every one of those is reachable without an account.

What

The request is bounded before it is read. A POST must carry a known, non-zero Content-Length under 2 MiB. Fetch negotiation is want/have lines and a single protocol-v2 round trip, so this is generous by two orders of magnitude — the cap is only possible because this is the read side; a push could never be bounded this way.

Only the two endpoints exist. GET info/refs?service=git-upload-pack and POST git-upload-pack, each with exactly one method. Anything else is a 404 before it reaches the CGI, so PATH_INFO containment no longer depends on git’s own handling of dot segments. An info/refs request without that exact service value is 404 too; receive-pack still answers 403 with the SSH hint, so a stray git push over HTTPS still gets told why.

The backend belongs to the request. exec.CommandContext on a context derived from r.Context() with a 10-minute budget (git_http_timeout_seconds), WaitDelay to bound the reap, and an explicit kill when writing the response fails. Closing the request body when the context ends is what unblocks the stdin copier: without it, the copier sits in Read on a dead connection and holds the process open behind it.

Load is bounded and counted. A transport semaphore (16 by default) and a per-client bound (8), refused as 503 + Retry-After rather than queued — a queue is one more thing that grows with load. The client address is the connection’s own unless the operator names a proxy header; last entry wins, because that is the one the nearest proxy appended. Both are config: git_http_max_concurrent, git_http_max_per_client, git_http_client_ip_header.

Server-level limits. ReadTimeout: 60s and MaxHeaderBytes: 64 KiB on both listeners. WriteTimeout stays unset on purpose — a clone, a release tarball, an LFS object and a site file are streamed, and a wall-clock limit on writes would truncate legitimate large downloads. Those streams are bounded where they are produced instead: the git transport by its own deadline, archives by archiveConc, downloads by the size caps from #3.

An off switch. git_http = false stops registering the /git/ routes altogether, for an instance whose repositories are all internal. Documented, including that LFS downloads share those URLs and go with them.

Tests

  • oversized body → 413; chunked → 411 (existing); zero-length → 413
  • the endpoint/method/service matrix, including git-upload-pack/../../..., ../demo.git/info/refs, dumb-HTTP paths, and receive-pack → 403
  • concurrency and per-client bounds → 503 with Retry-After, and the slot is returned on refusal
  • proxy header: ignored when unnamed, last entry used when named
  • a client that disconnects mid-fetch returns in ~0.1s instead of waiting for a timeout (fails on the old code, which blocks until the backend exits)
  • a fetch that never finishes is stopped on its budget and counted

gofmt, go vet ./..., go test ./..., and go test -race on the touched packages are clean; the end-to-end anonymous HTTP clone test still passes.

Discussion (0)

No discussion yet.