Files
member-console/internal/db/dsn_test.go
T
cgalo5758 ea4fee18b6 Fix five findings from security audit run 2
- Rotate the session token at the OIDC callback and restore the full
  lifetime; cap pre-auth sessions at 15 minutes and write no session
  for bare anonymous requests
- Treat db-dsn as a secret: accept db-dsn-file, log only host, port,
  database and user, and never echo a malformed DSN in an error
- Guard the logout callback with a state cookie so a forged visit
  cannot end a live session
- Collapse FedWiki site actions on a foreign tenant's domain to the
  not-found answer, as for a domain that does not exist
2026-09-09 20:53:31 -05:00

80 lines
2.7 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package db
import (
"strings"
"testing"
)
// attrsText renders every attribute's key and value into one string, so a
// test can assert a secret is absent from the whole set with one substring
// check instead of one per attribute.
func attrsText(t *testing.T) func(dsn string) string {
t.Helper()
return func(dsn string) string {
var b strings.Builder
for _, a := range dsnLogAttrs(dsn) {
b.WriteString(a.Key)
b.WriteByte('=')
b.WriteString(a.Value.String())
b.WriteByte(' ')
}
return b.String()
}
}
// TestDSNLogAttrs covers task 1.2/1.4 (security-audit-remediation-2, design
// D1, spec startup-configuration): the helper behind both "database
// connection established" log lines carries host, port, database and user,
// and never the password or the query string, however the password is
// shaped.
func TestDSNLogAttrs(t *testing.T) {
render := attrsText(t)
t.Run("reports host, port, database and user", func(t *testing.T) {
got := render("postgres://widget:hunter2@db.example.com:5432/widgets")
for _, want := range []string{"db_host=db.example.com", "db_port=5432", "db_name=widgets", "db_user=widget"} {
if !strings.Contains(got, want) {
t.Errorf("attrs = %q, missing %q", got, want)
}
}
})
t.Run("never returns a password, including one URL-encoded with @ and :", func(t *testing.T) {
// The password decodes to `p@ss:word` — the characters that force
// percent-encoding in the userinfo section of RFC 3986 section 3.2.1.
got := render("postgres://widget:p%40ss%3Aword@db.example.com:5432/widgets")
for _, secret := range []string{"p@ss:word", "p%40ss%3Aword", "hunter2"} {
if strings.Contains(got, secret) {
t.Errorf("attrs = %q, leaked password %q", got, secret)
}
}
if !strings.Contains(got, "db_user=widget") {
t.Errorf("attrs = %q, missing db_user", got)
}
})
t.Run("never returns the query string", func(t *testing.T) {
got := render("postgres://widget:hunter2@db.example.com:5432/widgets?sslmode=disable&search_path=core,public")
for _, leak := range []string{"sslmode", "disable", "search_path"} {
if strings.Contains(got, leak) {
t.Errorf("attrs = %q, leaked query content %q", got, leak)
}
}
})
t.Run("a DSN that fails to parse yields one attribute naming the failure, not the input", func(t *testing.T) {
dsn := "not a valid dsn secret-value"
attrs := dsnLogAttrs(dsn)
if len(attrs) != 1 {
t.Fatalf("attrs = %v, want exactly one", attrs)
}
got := render(dsn)
if strings.Contains(got, "secret-value") {
t.Errorf("attrs = %q, leaked the input DSN", got)
}
})
}