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.
Harden the web transport and bound the pages that run git
701232cf37157ff392e3b35a3279735d72e585d4 · Keenan Nemetz · 12d 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/web/web.go +7−2
| @@ -56,8 +56,12 @@ type Web struct { | |||
| 56 | 56 | // Started is the process start, shown on the status page as uptime. | |
| 57 | 57 | Started time.Time | |
| 58 | 58 | ||
| 59 | // sem bounds compressed archive streams; see releases.go. | ||
| 59 | // sem bounds archive streams; see releases.go. | ||
| 60 | 60 | sem chan struct{} | |
| 61 | // gitSem bounds the pages that shell out to git, and gitWait is how long a page | ||
| 62 | // waits for a slot; see gitbounds.go. | ||
| 63 | gitSem chan struct{} | ||
| 64 | gitWait time.Duration | ||
| 61 | 65 | ||
| 62 | 66 | // Development login, configured by dev_login. It is resolved once at startup | |
| 63 | 67 | // and only ever used for loopback requests; the session is created lazily. | |
| @@ -79,7 +83,8 @@ func New(repos *repo.Store, ix *index.Store, ident *identity.Store, rs *runs.Sto | |||
| 79 | 83 | } | |
| 80 | 84 | w := &Web{Repos: repos, Index: ix, Ident: ident, Runs: rs, Cfg: cfg, | |
| 81 | 85 | Auth: auth, Ceremonies: newCeremonyStore(), Started: time.Now().UTC(), | |
| 82 | sem: make(chan struct{}, archiveConc), limits: newAuthLimits()} | ||
| 86 | sem: make(chan struct{}, archiveConc), gitSem: make(chan struct{}, webGitConcurrent), | ||
| 87 | gitWait: webGitWait, limits: newAuthLimits()} | ||
| 83 | 88 | // The templates are parsed after the Web exists because two of the functions | |
| 84 | 89 | // they call (who, nav) read the roster through the Web: metadata files carry | |
| 85 | 90 | // account names, and a page should show the account name for either. | |