Skip to content
quarry Artifact [1bebfbb839]
Login

Artifact [1bebfbb839]

Artifact 1bebfbb8391f8cc3e2afe8ee3e7411178d0d853b52c1d466ab855f8548ecf76f:


package web

import (
	"os"
	"strings"
	"testing"
	"time"
)

// A minted secret survives exactly one collection. "Shown once" was a
// claim the query string could not keep: a reload re-read the URL and
// showed it again, and so did anyone holding the history entry.
func TestVaultedSecretIsCollectedOnce(t *testing.T) {
	v := newSecretVault()
	nonce := v.stash("dgn_abc_def")
	if nonce == "" || strings.Contains(nonce, "dgn_") {
		t.Fatalf("the nonce is not a stand-in for the secret: %q", nonce)
	}
	if got := v.take(nonce); got != "dgn_abc_def" {
		t.Fatalf("first collection got %q", got)
	}
	if got := v.take(nonce); got != "" {
		t.Errorf("a second collection still yielded %q", got)
	}
}

// Nothing to show means nothing in the URL, rather than a nonce that
// resolves to an empty string and a page claiming a secret exists.
func TestVaultRefusesToStashNothing(t *testing.T) {
	v := newSecretVault()
	if n := v.stash(""); n != "" {
		t.Errorf("an empty secret got nonce %q", n)
	}
	if got := v.take(""); got != "" {
		t.Errorf("an empty nonce yielded %q", got)
	}
	if got := v.take("cafebabe"); got != "" {
		t.Errorf("an unknown nonce yielded %q", got)
	}
}

// An uncollected secret does not sit in memory indefinitely, and an
// expired one is not handed over late.
func TestVaultedSecretExpires(t *testing.T) {
	v := newSecretVault()
	nonce := v.stash("s3cret")
	v.mu.Lock()
	e := v.m[nonce]
	e.at = time.Now().Add(-secretTTL - time.Second)
	v.m[nonce] = e
	v.mu.Unlock()
	if got := v.take(nonce); got != "" {
		t.Errorf("an expired secret was handed over: %q", got)
	}
	v.mu.Lock()
	n := len(v.m)
	v.mu.Unlock()
	if n != 0 {
		t.Errorf("expired entries were kept: %d", n)
	}
}

// Two mints do not collide, and collecting one does not disturb the
// other — a forge mints for several repositories at once.
func TestVaultKeepsMintsApart(t *testing.T) {
	v := newSecretVault()
	a, b := v.stash("first"), v.stash("second")
	if a == b {
		t.Fatal("two mints share a nonce")
	}
	if got := v.take(b); got != "second" {
		t.Fatalf("wrong secret for b: %q", got)
	}
	if got := v.take(a); got != "first" {
		t.Errorf("collecting b disturbed a: %q", got)
	}
}

// The regression itself: no code path may put a minted credential into
// a URL. This reads the source rather than a rendered page, because the
// leak was in what the handler HANDED the browser, and every one of the
// four sites looked reasonable on its own.
func TestNoMintedSecretTravelsInAQueryString(t *testing.T) {
	src := readWebSource(t)
	for _, bad := range []string{
		`q.Set("s", sub.Secret)`,
		`q.Set("s", secret)`,
		`"?rt="+url.QueryEscape(tk.Token)`,
		`"?wt="+url.QueryEscape(tok.Token)`,
	} {
		if strings.Contains(src, bad) {
			t.Errorf("a credential is back in a URL: %s", bad)
		}
	}
	// And the collectors go through the vault, so a stale link cannot
	// re-reveal what it once carried.
	for _, want := range []string{
		`mintedSecrets.take(req.URL.Query().Get("s"))`,
		`mintedSecrets.take(req.URL.Query().Get("rt"))`,
		`mintedSecrets.take(req.URL.Query().Get("wt"))`,
	} {
		if !strings.Contains(src, want) {
			t.Errorf("a secret is not collected through the vault: %s", want)
		}
	}
}

// readWebSource reads this package's main source, so a test can assert
// on what the handlers hand the browser rather than on a rendered page.
func readWebSource(t *testing.T) string {
	t.Helper()
	b, err := os.ReadFile("web.go")
	if err != nil {
		t.Fatal(err)
	}
	return string(b)
}