- 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
80 lines
2.7 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|