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 sign-in input bounds and private response caching
25343521418945b250da2fb29d27cffe34ee2a6e · Keenan Nemetz · 3d ago · parent b492b557
Harden sign-in input bounds and private response caching Bound the account name in LoginBegin before it reaches the per-account rate limiter or the ceremony store, and bound the passkey label and the credential fields copied from an authenticator. Rate-limit keys are capped by hashing a long value, forwarding headers used as addresses are capped, and expired limiter entries are swept as the table grows. Mark downloads and rendered pages private unless the repository is readable anonymously. A private repository's artifact, archive, run artifact, raw file, and HTML pages previously either carried a public cache directive or no directive at all, so a shared cache could serve them to an anonymous reader. Refs #209 (01M6S78Y7EQY596Y9TQRSNRVSE). Refs #210 (01M6S7936QAYM9TMSCXCYP2CF9).
server/web/ratelimit.go +30−1
| @@ -17,6 +17,8 @@ package web | |||
| 17 | 17 | // many hosts from getting a faster answer than the same guess from one. | |
| 18 | 18 | ||
| 19 | 19 | import ( | |
| 20 | "crypto/sha256" | ||
| 21 | "encoding/hex" | ||
| 20 | 22 | "fmt" | |
| 21 | 23 | "net" | |
| 22 | 24 | "net/http" | |
| @@ -51,6 +53,18 @@ const ( | |||
| 51 | 53 | // cleared rather than grown, because the alternative is memory an anonymous | |
| 52 | 54 | // client chooses. | |
| 53 | 55 | maxLimitKeys = 1 << 16 | |
| 56 | // limitSweepEvery bounds how long expired keys can sit in the table before a | ||
| 57 | // sweep. Without it a table below the cap is only swept when it becomes full, | ||
| 58 | // so a client can grow it one key at a time for as long as it likes. | ||
| 59 | limitSweepEvery = 1024 | ||
| 60 | // maxRateKeyBytes bounds what one key can add to the table. Account names and | ||
| 61 | // addresses are short; a key longer than this is hashed so a client-supplied | ||
| 62 | // string cannot decide how much memory the map holds. | ||
| 63 | maxRateKeyBytes = 256 | ||
| 64 | // maxClientAddressBytes bounds the proxy header before it is used as a key or | ||
| 65 | // written to the log. The header is client-influenced when a proxy does not | ||
| 66 | // replace it, and the rate limit is not the place to trust its length. | ||
| 67 | maxClientAddressBytes = 64 | ||
| 54 | 68 | ) | |
| 55 | 69 | ||
| 56 | 70 | // rateLimiter counts events per key inside one window. | |
| @@ -77,6 +91,7 @@ func (l *rateLimiter) allow(key string) (bool, time.Duration) { | |||
| 77 | 91 | if l == nil || l.limit <= 0 || key == "" { | |
| 78 | 92 | return true, 0 | |
| 79 | 93 | } | |
| 94 | key = rateKey(key) | ||
| 80 | 95 | now := l.now().UTC() | |
| 81 | 96 | l.mu.Lock() | |
| 82 | 97 | defer l.mu.Unlock() | |
| @@ -87,7 +102,7 @@ func (l *rateLimiter) allow(key string) (bool, time.Duration) { | |||
| 87 | 102 | e.count++ | |
| 88 | 103 | return true, 0 | |
| 89 | 104 | } | |
| 90 | if len(l.m) >= maxLimitKeys { | ||
| 105 | if len(l.m) >= maxLimitKeys || (len(l.m) > 0 && len(l.m)%limitSweepEvery == 0) { | ||
| 91 | 106 | l.sweep(now) | |
| 92 | 107 | } | |
| 93 | 108 | l.m[key] = &rateEntry{count: 1, until: now.Add(l.window)} | |
| @@ -154,6 +169,17 @@ func (w *Web) limitForm(rw http.ResponseWriter, r *http.Request, l *rateLimiter, | |||
| 154 | 169 | return false | |
| 155 | 170 | } | |
| 156 | 171 | ||
| 172 | // rateKey bounds the key that reaches the map. A short key is stored as written | ||
| 173 | // so the common case stays readable in a test or a trace; a longer one is hashed | ||
| 174 | // to a fixed size so its length cannot become the table's memory use. | ||
| 175 | func rateKey(key string) string { | ||
| 176 | if len(key) <= maxRateKeyBytes { | ||
| 177 | return key | ||
| 178 | } | ||
| 179 | sum := sha256.Sum256([]byte(key)) | ||
| 180 | return "sha256:" + hex.EncodeToString(sum[:]) | ||
| 181 | } | ||
| 182 | |||
| 157 | 183 | // clientAddress is the key the address budgets count under. Behind a trusted proxy | |
| 158 | 184 | // the configured header is the client; without one it is the connection. The same | |
| 159 | 185 | // header drives the anonymous Git transport's per-client limit, so an instance | |
| @@ -167,6 +193,9 @@ func (w *Web) clientAddress(r *http.Request) string { | |||
| 167 | 193 | if i := strings.IndexByte(v, ','); i >= 0 { | |
| 168 | 194 | v = strings.TrimSpace(v[:i]) | |
| 169 | 195 | } | |
| 196 | if len(v) > maxClientAddressBytes { | ||
| 197 | v = v[:maxClientAddressBytes] | ||
| 198 | } | ||
| 170 | 199 | return v | |
| 171 | 200 | } | |
| 172 | 201 | } | |