From a83088e7e615142b93dfffbbd58ac8b15dfed7fe Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Tue, 19 May 2026 17:05:20 +0200 Subject: [PATCH] fix(audit): strip IPv6 brackets before INET insert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a client connects over IPv6 loopback, net/http's RemoteAddr is '[::1]:port'. The previous clientIP() returned '[::1]' (brackets and all) which Postgres's INET type rejects with 'invalid input syntax for type inet: "[::1]" (SQLSTATE 22P02)'. Live local-smoke caught this — every state-changing endpoint emitted the audit event, the INSERT rolled back, and a warning landed in the log. The user-facing operation succeeded so the caller never noticed, but audit_log stayed empty. Fix: - Use net.SplitHostPort which returns IPv6 hosts without brackets. - Add stripBrackets() as a belt-and-braces for X-Forwarded-For headers that wrap the IP themselves (some proxies do). Refs: M4.2 --- internal/server/helpers.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/server/helpers.go b/internal/server/helpers.go index f2f7bbb..3a2baab 100644 --- a/internal/server/helpers.go +++ b/internal/server/helpers.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "log/slog" + "net" "net/http" "strings" "time" @@ -87,22 +88,23 @@ func (s *statusRecorder) WriteHeader(c int) { func clientIP(r *http.Request) string { if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { if i := strings.IndexByte(fwd, ','); i > 0 { - return strings.TrimSpace(fwd[:i]) + return stripBrackets(strings.TrimSpace(fwd[:i])) } - return strings.TrimSpace(fwd) + return stripBrackets(strings.TrimSpace(fwd)) } - if host, _, ok := splitHostPort(r.RemoteAddr); ok { + if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + // net.SplitHostPort returns IPv6 without brackets already. return host } - return r.RemoteAddr + return stripBrackets(r.RemoteAddr) } -// splitHostPort is a port-tolerant version of net.SplitHostPort that doesn't -// error on missing port. -func splitHostPort(s string) (string, string, bool) { - i := strings.LastIndexByte(s, ':') - if i < 0 { - return s, "", false +// stripBrackets removes the `[...]` wrapping IPv6 hosts pick up from +// net/http's RemoteAddr in some Go versions, since Postgres `inet` rejects +// `[::1]` but accepts `::1`. +func stripBrackets(s string) string { + if len(s) >= 2 && s[0] == '[' && s[len(s)-1] == ']' { + return s[1 : len(s)-1] } - return s[:i], s[i+1:], true + return s }