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 · 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.
core/git/git.go +20−1
| @@ -6,6 +6,7 @@ package git | |||
| 6 | 6 | import ( | |
| 7 | 7 | "bufio" | |
| 8 | 8 | "bytes" | |
| 9 | "context" | ||
| 9 | 10 | "errors" | |
| 10 | 11 | "fmt" | |
| 11 | 12 | "io" | |
| @@ -33,10 +34,23 @@ type Repo struct { | |||
| 33 | 34 | // inspecting the very repository being pushed, whose new objects are only | |
| 34 | 35 | // visible through GIT_QUARANTINE_PATH. | |
| 35 | 36 | ExtraEnv []string | |
| 37 | // ctx, when set, bounds every git invocation this handle starts. A web page | ||
| 38 | // that shells out to git needs one: a repository can be slow, and the process | ||
| 39 | // is the thing that has to stop. | ||
| 40 | ctx context.Context | ||
| 36 | 41 | } | |
| 37 | 42 | ||
| 38 | 43 | func Open(dir string) *Repo { return &Repo{Dir: dir} } | |
| 39 | 44 | ||
| 45 | // WithContext returns a handle whose git invocations are killed when ctx ends. It | ||
| 46 | // is a copy, so a caller cannot change the deadline of a handle somebody else | ||
| 47 | // holds. | ||
| 48 | func (r *Repo) WithContext(ctx context.Context) *Repo { | ||
| 49 | cp := *r | ||
| 50 | cp.ctx = ctx | ||
| 51 | return &cp | ||
| 52 | } | ||
| 53 | |||
| 40 | 54 | func (r *Repo) run(stdin []byte, args ...string) ([]byte, string, error) { | |
| 41 | 55 | return r.runEnv(nil, stdin, args...) | |
| 42 | 56 | } | |
| @@ -68,7 +82,12 @@ func (r *Repo) Run(stdin []byte, args ...string) ([]byte, error) { | |||
| 68 | 82 | ||
| 69 | 83 | // command builds a git invocation with the environment every call shares. | |
| 70 | 84 | func (r *Repo) command(args ...string) *exec.Cmd { | |
| 71 | cmd := exec.Command("git", args...) | ||
| 85 | var cmd *exec.Cmd | ||
| 86 | if r.ctx != nil { | ||
| 87 | cmd = exec.CommandContext(r.ctx, "git", args...) | ||
| 88 | } else { | ||
| 89 | cmd = exec.Command("git", args...) | ||
| 90 | } | ||
| 72 | 91 | cmd.Env = append(scrubbedEnv(), | |
| 73 | 92 | "GIT_DIR="+r.Dir, | |
| 74 | 93 | "GIT_TERMINAL_PROMPT=0", | |