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/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.

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