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.
Harden the web transport and bound the pages that run git
701232cf37157ff392e3b35a3279735d72e585d4 · Keenan Nemetz · 13d ago · parent 6bfc4294
Harden the web transport and bound the pages that run git Generated clone and LFS download links were derived from the request's Host header, with X-Forwarded-Proto believed from anyone. Those links are copied out of a page and pasted into a terminal, and the LFS hrefs are followed by a client, so a forged Host chose where somebody else's download went. Links now use public_url when it is configured, which is how a production instance is meant to be set up; without it the request origin is all there is, and forwarded headers are honoured only when git_http_client_ip_header is set, because that is the instance's statement that a proxy it trusts rewrites them. Site links follow the forge's scheme instead of always saying http, so an HTTPS forge does not link to a mixed-content site. The security headers now close the rest of the surface: object-src is none, inline styles are no longer allowed (no template or generated markup contains one), and Permissions-Policy, COOP and CORP are set. The CSRF check gained a second lock: when the browser says where a request came from, that origin must be this instance, on form posts and on the JSON ceremony endpoints alike. A missing Origin and Referer is not evidence, so only a request that names somewhere else is refused. The pages that shell out to git are bounded. Search, compare, blame, file history, commit history and both archive formats take a slot from a pool of eight and run under a 30-second deadline, which is attached to the git handle so the process is killed rather than left running after the reader has gone. Past a five-second wait for a slot the page answers 503 with Retry-After. Plain tar now takes the archive slot too: it is cheaper than gzip, not free, and both stream for as long as the client reads. Tests: clone URLs ignore a forged Host when public_url is set and honour forwarded headers only behind a declared proxy; LFS hrefs likewise; site links follow the scheme; the CSP has no unsafe-inline and names object-src; a saturated git pool answers 503 with Retry-After; a cross-origin post is refused with a valid CSRF token, end to end; the JSON endpoints refuse a foreign origin. Docs: public_url now explains that it decides generated links and that forwarded headers are only trusted with the proxy header set; Sites gains a same-site risk section recommending a separate registrable domain when sites are not trusted.
server/lfs.go +21−10
| @@ -147,16 +147,27 @@ func (s *Server) LFSObject(w http.ResponseWriter, r *http.Request) { | |||
| 147 | 147 | } | |
| 148 | 148 | } | |
| 149 | 149 | ||
| 150 | // lfsObjectBase is where download hrefs point. It is derived from the request | ||
| 151 | // rather than configured, so a clone through a proxy hands back a URL on the | ||
| 152 | // same host the client already uses. | ||
| 150 | // lfsObjectBase is where download hrefs point. The configured public_url wins when | ||
| 151 | // there is one: these hrefs are followed by git-lfs, so a Host header an attacker | ||
| 152 | // chose would send the client to a host the attacker chose. Without a public_url | ||
| 153 | // the origin the client used is the only thing there is, and the forwarding header | ||
| 154 | // is believed only when the instance declares a trusted proxy. | ||
| 153 | 155 | func (s *Server) lfsObjectBase(r *http.Request, name string) string { | |
| 154 | scheme := "http" | ||
| 155 | if r.TLS != nil { | ||
| 156 | scheme = "https" | ||
| 157 | } | ||
| 158 | if p := r.Header.Get("X-Forwarded-Proto"); p == "https" || p == "http" { | ||
| 159 | scheme = p | ||
| 156 | base := "" | ||
| 157 | if s.Cfg != nil { | ||
| 158 | base = s.Cfg.BaseURL() | ||
| 159 | } | ||
| 160 | if base == "" { | ||
| 161 | scheme := "http" | ||
| 162 | if r.TLS != nil { | ||
| 163 | scheme = "https" | ||
| 164 | } | ||
| 165 | if s.Cfg != nil && strings.TrimSpace(s.Cfg.GitHTTPClientIPHeader) != "" { | ||
| 166 | if p := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))); p == "https" || p == "http" { | ||
| 167 | scheme = p | ||
| 168 | } | ||
| 169 | } | ||
| 170 | base = scheme + "://" + r.Host | ||
| 160 | 171 | } | |
| 161 | return scheme + "://" + r.Host + "/git/" + name + ".git" + objectShard + "objects" | ||
| 172 | return base + "/git/" + name + ".git" + objectShard + "objects" | ||
| 162 | 173 | } | |