chore: clean .gitignore cache
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
)
|
||||
|
||||
// ConstantTimeCompare compares two strings in constant time to prevent comparison timing attacks.
|
||||
//
|
||||
// PROTECTION SCOPE:
|
||||
// This function protects against timing attacks during the comparison operation itself,
|
||||
// where an attacker might measure how long it takes to compare two strings byte-by-byte
|
||||
// to infer information about the expected value.
|
||||
//
|
||||
// IMPORTANT LIMITATIONS:
|
||||
// This does NOT protect against timing variance in database queries. If you retrieve a token
|
||||
// from the database (e.g., WHERE invite_token = ?), the DB query timing will vary based on
|
||||
// whether the token exists, potentially revealing information to an attacker through timing analysis.
|
||||
// See backend/internal/api/handlers/user_handler.go for examples of this limitation.
|
||||
//
|
||||
// DEFENSE-IN-DEPTH:
|
||||
// Despite this limitation, using constant-time comparison is still valuable as part of a
|
||||
// defense-in-depth strategy. It eliminates one potential timing leak and should be used
|
||||
// when comparing sensitive values like API keys, tokens, or passwords that are already
|
||||
// in memory.
|
||||
//
|
||||
// Returns true if the strings are equal, false otherwise.
|
||||
func ConstantTimeCompare(a, b string) bool {
|
||||
aBytes := []byte(a)
|
||||
bBytes := []byte(b)
|
||||
|
||||
// subtle.ConstantTimeCompare returns 1 if equal, 0 if not
|
||||
return subtle.ConstantTimeCompare(aBytes, bBytes) == 1
|
||||
}
|
||||
|
||||
// ConstantTimeCompareBytes compares two byte slices in constant time to prevent comparison timing attacks.
|
||||
//
|
||||
// This function has the same protection scope and limitations as ConstantTimeCompare.
|
||||
// See ConstantTimeCompare documentation for details on what this protects against and
|
||||
// what it does NOT protect against (e.g., database query timing variance).
|
||||
func ConstantTimeCompareBytes(a, b []byte) bool {
|
||||
return subtle.ConstantTimeCompare(a, b) == 1
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConstantTimeCompare(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
expected bool
|
||||
}{
|
||||
{"equal strings", "secret123", "secret123", true},
|
||||
{"different strings", "secret123", "secret456", false},
|
||||
{"different lengths", "short", "muchlonger", false},
|
||||
{"empty strings", "", "", true},
|
||||
{"one empty", "notempty", "", false},
|
||||
{"unicode equal", "héllo", "héllo", true},
|
||||
{"unicode different", "héllo", "hëllo", false},
|
||||
{"special chars equal", "!@#$%^&*()", "!@#$%^&*()", true},
|
||||
{"whitespace matters", "hello ", "hello", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ConstantTimeCompare(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ConstantTimeCompare(%q, %q) = %v, want %v", tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantTimeCompareBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
a []byte
|
||||
b []byte
|
||||
expected bool
|
||||
}{
|
||||
{"equal bytes", []byte{1, 2, 3}, []byte{1, 2, 3}, true},
|
||||
{"different bytes", []byte{1, 2, 3}, []byte{1, 2, 4}, false},
|
||||
{"different lengths", []byte{1, 2}, []byte{1, 2, 3}, false},
|
||||
{"empty slices", []byte{}, []byte{}, true},
|
||||
{"nil slices", nil, nil, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ConstantTimeCompareBytes(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ConstantTimeCompareBytes(%v, %v) = %v, want %v", tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConstantTimeCompare ensures the function remains constant-time.
|
||||
func BenchmarkConstantTimeCompare(b *testing.B) {
|
||||
secret := "a]3kL9#mP2$vN7@qR5*wX1&yT4^uI8%oE0!"
|
||||
|
||||
b.Run("equal", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ConstantTimeCompare(secret, secret)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("different_first_char", func(b *testing.B) {
|
||||
different := "b]3kL9#mP2$vN7@qR5*wX1&yT4^uI8%oE0!"
|
||||
for i := 0; i < b.N; i++ {
|
||||
ConstantTimeCompare(secret, different)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("different_last_char", func(b *testing.B) {
|
||||
different := "a]3kL9#mP2$vN7@qR5*wX1&yT4^uI8%oE0?"
|
||||
for i := 0; i < b.N; i++ {
|
||||
ConstantTimeCompare(secret, different)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Package util provides utility functions used across the application.
|
||||
package util
|
||||
|
||||
import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SanitizeForLog removes control characters and newlines from user content before logging.
|
||||
func SanitizeForLog(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
s = strings.ReplaceAll(s, "\r\n", " ")
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
re := regexp.MustCompile(`[\x00-\x1F\x7F]+`)
|
||||
s = re.ReplaceAllString(s, " ")
|
||||
return s
|
||||
}
|
||||
|
||||
// CanonicalizeIPForSecurity normalizes an IP string for security decisions
|
||||
// (rate limiting keys, allow-list CIDR checks, etc.). It preserves Gin's
|
||||
// trust proxy behavior by operating on the already-resolved client IP string.
|
||||
//
|
||||
// Normalizations:
|
||||
// - IPv6 loopback (::1) -> 127.0.0.1 (stable across IPv4/IPv6 localhost)
|
||||
// - IPv4-mapped IPv6 (e.g. ::ffff:127.0.0.1) -> 127.0.0.1
|
||||
func CanonicalizeIPForSecurity(ipStr string) string {
|
||||
ipStr = strings.TrimSpace(ipStr)
|
||||
if ipStr == "" {
|
||||
return ipStr
|
||||
}
|
||||
|
||||
// Defensive normalization in case the input is not a plain IP string.
|
||||
// Gin's Context.ClientIP() should return an IP, but in proxy/test setups
|
||||
// we may still see host:port or comma-separated values.
|
||||
if idx := strings.IndexByte(ipStr, ','); idx >= 0 {
|
||||
ipStr = strings.TrimSpace(ipStr[:idx])
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(ipStr); err == nil {
|
||||
ipStr = host
|
||||
}
|
||||
ipStr = strings.Trim(ipStr, "[]")
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return ipStr
|
||||
}
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
return v4.String()
|
||||
}
|
||||
if ip.IsLoopback() {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSanitizeForLog(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "clean string",
|
||||
input: "Hello World",
|
||||
expected: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "string with newline",
|
||||
input: "Hello\nWorld",
|
||||
expected: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "string with carriage return and newline",
|
||||
input: "Hello\r\nWorld",
|
||||
expected: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "string with multiple newlines",
|
||||
input: "Hello\nWorld\nTest",
|
||||
expected: "Hello World Test",
|
||||
},
|
||||
{
|
||||
name: "string with control characters",
|
||||
input: "Hello\x00\x01\x1FWorld",
|
||||
expected: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "string with DEL character (0x7F)",
|
||||
input: "Hello\x7FWorld",
|
||||
expected: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "complex string with mixed control chars",
|
||||
input: "Line1\r\nLine2\nLine3\x00\x01\x7F",
|
||||
expected: "Line1 Line2 Line3 ",
|
||||
},
|
||||
{
|
||||
name: "string with tabs (0x09 is control char)",
|
||||
input: "Hello\tWorld",
|
||||
expected: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "string with only control chars",
|
||||
input: "\x00\x01\x02\x1F\x7F",
|
||||
expected: " ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeForLog(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeForLog(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user