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), | |