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/server.go +20−4

@@ -76,9 +76,13 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error)
76 76 }
77 77 }
78 78 proxy := githttp.New(githttp.Config{
79 Root: cfg.ReposDir(),
80 Open: inst.Repos.Open,
81 Log: log,
79 Root: cfg.ReposDir(),
80 Open: inst.Repos.Open,
81 Log: log,
82 MaxConcurrent: cfg.GitHTTPMaxConcurrent,
83 MaxPerClient: cfg.GitHTTPMaxPerClient,
84 ClientIPHeader: cfg.GitHTTPClientIPHeader,
85 RequestTimeout: time.Duration(cfg.GitHTTPTimeoutSeconds) * time.Second,
82 86 })
83 87 ssh, err := ssh.New(ssh.Config{
84 88 Identity: inst.Identity,
@@ -190,7 +194,17 @@ func (s *Server) Run(ctx context.Context) error {
190 194 httpSrv := &http.Server{
191 195 Handler: s.Handler(),
192 196 ReadHeaderTimeout: 15 * time.Second,
193 IdleTimeout: 120 * time.Second,
197 // ReadTimeout bounds the request, body included. The only bodies on the
198 // HTTP surface are a login form, a comment and a fetch negotiation, all of
199 // which are small and arrive long before this.
200 ReadTimeout: 60 * time.Second,
201 // WriteTimeout is deliberately unset: a clone, a release tarball, an LFS
202 // object and a site file are streamed, and a wall-clock limit on writing the
203 // response would truncate a legitimate large download. Those streams are
204 // bounded where they are produced instead — the git transport by its own
205 // request deadline, archives by a concurrency bound, downloads by a size cap.
206 MaxHeaderBytes: 64 << 10,
207 IdleTimeout: 120 * time.Second,
194 208 }
195 209 var httpsLn net.Listener
196 210 var httpsSrv *http.Server
@@ -206,6 +220,8 @@ func (s *Server) Run(ctx context.Context) error {
206 220 Handler: s.Handler(),
207 221 TLSConfig: s.acme.tlsConfig(),
208 222 ReadHeaderTimeout: 15 * time.Second,
223 ReadTimeout: 60 * time.Second,
224 MaxHeaderBytes: 64 << 10,
209 225 IdleTimeout: 120 * time.Second,
210 226 }
211 227 }