GL-7 Part 1: passphrase hardening on the customer page (security)

The Setup Command panel rendered the per-customer retrieval passphrase (the
secret that fetches the WHOLE config) in cleartext twice — as #retrieval-pw
text and baked into the Option-3 curl's X-Retrieval-Password header — which
contradicts the panel's own "never on the command line" guidance. Now:
the retrieval password is MASKED by default (bullet run) with Reveal/Hide +
copy-secret controls (value lives in data-secret — the existing reveal
model); the Option-3 debug command carries a <YOUR-RETRIEVAL-PASSWORD>
placeholder, never the secret. Render test asserts the secret is not baked
into any command + is masked by default; red-proof (bake it back) FAILS.
(A zero-secret-in-DOM reveal-on-demand fetch is a noted follow-up, not this task.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 08:23:41 +02:00
parent fb8e5111cf
commit b93dd03f0a
2 changed files with 93 additions and 4 deletions
+61
View File
@@ -113,3 +113,64 @@ func TestTemplates_DashboardCriticalBadge(t *testing.T) {
t.Errorf("critical badge renders AFTER the error badge (crit@%d, err@%d) — must be first", critIdx, errIdx)
}
}
// GL-7 Part 1: the retrieval passphrase must be MASKED by default and NEVER baked into a copyable
// command block. The value still ships in data-secret (the reveal/copy mechanism — the existing
// model), but the Option-3 debug curl must carry a placeholder, not the secret.
// RED-PROOF: revert the Option-3 block to `X-Retrieval-Password: {{.Config.RetrievalPassword}}`
// (or the #retrieval-pw code back to the raw value) → the secret appears in a command / unmasked →
// the "not in the -H command" / masked assertions FAIL.
func TestTemplates_PassphraseHardened(t *testing.T) {
st, err := store.New(filepath.Join(t.TempDir(), "t.db"), log.New(io.Discard, "", 0))
if err != nil {
t.Fatalf("store.New: %v", err)
}
t.Cleanup(func() { st.Close() })
const secret = "correct-horse-battery-staple-9f2a"
if err := st.SaveCustomerConfig(&store.CustomerConfig{
CustomerID: "peti-felhom", CustomerName: "Peti", Domain: "sajatfelhom.hu",
RetrievalPassword: secret, APIKey: "apikey-xyz", Status: "active",
}); err != nil {
t.Fatalf("SaveCustomerConfig: %v", err)
}
s := New(st, "", "", "test", time.Hour, log.New(io.Discard, "", 0))
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/configs/peti-felhom", nil)
s.handleCustomerUnified(rr, req, "peti-felhom")
if rr.Code != 200 {
t.Fatalf("customer page status = %d", rr.Code)
}
html := rr.Body.String()
// (a) the secret must NOT appear inside the Option-3 curl command (no X-Retrieval-Password: <secret>).
if strings.Contains(html, "X-Retrieval-Password: "+secret) {
t.Errorf("passphrase is baked into the Option-3 command (must be a placeholder)")
}
// The Option-3 command carries the placeholder instead.
if !strings.Contains(html, "YOUR-RETRIEVAL-PASSWORD") {
t.Errorf("Option-3 command missing the passphrase placeholder")
}
// (b) the visible retrieval-pw node is masked by default (bullets), not the cleartext value.
if !strings.Contains(html, `id="retrieval-pw"`) {
t.Fatalf("retrieval-pw node missing")
}
// the reveal control + copy-secret wiring must be present (the value lives in data-secret).
if !strings.Contains(html, `onclick="toggleSecret('retrieval-pw')"`) ||
!strings.Contains(html, `onclick="copySecret('retrieval-pw')"`) {
t.Errorf("reveal/copy-secret controls missing")
}
if !strings.Contains(html, `data-secret="`+secret+`"`) {
t.Errorf("data-secret not populated for the reveal control")
}
// the DEFAULT visible masked text is a run of bullet entities; the raw secret is only in
// data-secret, never the code node's visible text content.
i := strings.Index(html, `id="retrieval-pw"`)
codeText := html[i : strings.Index(html[i:], "</code>")+i]
if !strings.Contains(codeText, "&#x2022;&#x2022;&#x2022;") {
t.Errorf("retrieval-pw is not masked by default (no bullet-entity run)")
}
if strings.Contains(codeText[strings.Index(codeText, ">")+1:], secret) {
t.Errorf("raw secret is the retrieval-pw node's visible default text (must be masked)")
}
}