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/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 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/auth_hardening_test.go +39

@@ -328,3 +328,42 @@ func TestOnlyUntrustedWritesAreBudgeted(t *testing.T) {
328 328 }
329 329 }
330 330 }
331
332 // TestLoginBeginBoundsTheAccountName: an invalid name gets the same decoy as an
333 // unknown account, but it does not reach the rate limiter or ceremony store at
334 // whatever length the client sent.
335 func TestLoginBeginBoundsTheAccountName(t *testing.T) {
336 t.Parallel()
337 w := authWeb(t)
338 huge := strings.Repeat("a", 1<<19)
339 r := httptest.NewRequest("POST", "/auth/login/begin",
340 strings.NewReader(`{"username":"`+huge+`"}`))
341 r.RemoteAddr = "127.0.0.1:1234"
342 rw := httptest.NewRecorder()
343 w.LoginBegin(rw, r)
344 if rw.Code != http.StatusOK {
345 t.Fatalf("oversized name = %d %s, want the decoy 200", rw.Code, rw.Body.String())
346 }
347 if !strings.Contains(rw.Body.String(), `"allowCredentials"`) {
348 t.Fatalf("the decoy is not shaped like an assertion request: %s", rw.Body.String())
349 }
350 w.limits.beginAccount.mu.Lock()
351 defer w.limits.beginAccount.mu.Unlock()
352 for key := range w.limits.beginAccount.m {
353 if len(key) > maxRateKeyBytes {
354 t.Fatalf("the limiter stored a %d-byte key, limit %d", len(key), maxRateKeyBytes)
355 }
356 }
357 }
358
359 // TestDynamicPagesArePrivate: a templated page is rendered for the viewer who
360 // asked for it, so a shared cache must not keep it.
361 func TestDynamicPagesArePrivate(t *testing.T) {
362 t.Parallel()
363 w := newTestWeb(t)
364 rw := httptest.NewRecorder()
365 w.notFound(rw, httptest.NewRequest("GET", "/r/example/issues/nope", nil))
366 if got := rw.Header().Get("Cache-Control"); !strings.Contains(got, "private") {
367 t.Fatalf("Cache-Control = %q, want a private directive", got)
368 }
369 }