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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Bound the anonymous Git HTTP read path

86991e7020a418a08a5e3a36ce8f296561392e3b · Keenan Nemetz · 13d ago · parent e05f1c48

Bound the anonymous Git HTTP read path

The transport that needs no account had no bound on anything: the request body
was capped only at "not negative", the backend ran without a request context or
deadline, the path check accepted any suffix after `git-upload-pack` and handed
it to the CGI as `PATH_INFO`, and the server set no `ReadTimeout` and no
`MaxHeaderBytes`.

Bound what the read side can spend, which is every dimension available to a
fetch and none of the ones a push would need: a 2 MiB body cap with a required
`Content-Length`, exactly two endpoints with one method and one service each so
the CGI never sees an unvalidated path, `exec.CommandContext` plus `WaitDelay`
plus an explicit kill so a disconnect or an expired budget stops git instead of
leaving it running, and a concurrency semaphore with a per-client bound that
answers 503 with Retry-After rather than queueing.

The client address is the connection's own unless the operator names a proxy
header, because behind a proxy the connection address is the proxy. Both limits
and the header name are configuration, `git_http` turns the transport off
entirely, and `status` lists them so an operator can see what is in force.

`WriteTimeout` is deliberately not set: clone, archives, LFS objects and site
files are streamed, so those flows are bounded where they are produced.

server/web/status.go +19

@@ -305,6 +305,18 @@ func (w *Web) statusConfig() *statusConfigView {
305 305 if cfg.LFSWarnBytes == 0 {
306 306 lfsWarnNote = "warning disabled"
307 307 }
308 gitHTTPNote := ""
309 if !cfg.GitHTTP {
310 gitHTTPNote = "SSH only"
311 }
312 clientIPNote := "connection address"
313 if cfg.GitHTTPClientIPHeader != "" {
314 clientIPNote = "from " + cfg.GitHTTPClientIPHeader
315 }
316 perClientNote := "unlimited"
317 if cfg.GitHTTPMaxPerClient > 0 {
318 perClientNote = ""
319 }
308 320
309 321 v.Groups = []statusSettingGroup{
310 322 {Name: "Instance and access", Settings: []statusSetting{
@@ -331,6 +343,13 @@ func (w *Web) statusConfig() *statusConfigView {
331 343 configSetting("lfs_object_max_bytes", byteSetting(cfg.LFSObjectMaxBytes), ""),
332 344 configSetting("lfs_warn_bytes", byteSetting(cfg.LFSWarnBytes), lfsWarnNote),
333 345 }},
346 {Name: "Anonymous read limits", Settings: []statusSetting{
347 configSetting("git_http", yesNo(cfg.GitHTTP), gitHTTPNote),
348 configSetting("git_http_max_concurrent", fmt.Sprintf("%d", cfg.GitHTTPMaxConcurrent), ""),
349 configSetting("git_http_max_per_client", fmt.Sprintf("%d", cfg.GitHTTPMaxPerClient), perClientNote),
350 configSetting("git_http_client_ip_header", cfg.GitHTTPClientIPHeader, clientIPNote),
351 configSetting("git_http_timeout_seconds", fmt.Sprintf("%d seconds", cfg.GitHTTPTimeoutSeconds), ""),
352 }},
334 353 {Name: "CI", Settings: []statusSetting{
335 354 configSetting("lease_seconds", fmt.Sprintf("%d seconds", int(cfg.Lease().Seconds())), leaseNote),
336 355 configSetting("max_jobs_per_repo", fmt.Sprintf("%d", cfg.MaxJobsPerRepo), jobsNote),