diff --git a/backend/handlers/user/guest.go b/backend/handlers/user/guest.go
index e2c01fe..4cf8140 100644
--- a/backend/handlers/user/guest.go
+++ b/backend/handlers/user/guest.go
@@ -6,7 +6,6 @@ import (
"errors"
"log"
"net/http"
- "net/mail"
"regexp"
"strings"
@@ -69,10 +68,9 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
return
}
- // Validate email format (contains @ and .)
- _, err := mail.ParseAddress(req.Email)
- if err != nil {
- http.Error(w, "invalid email format", http.StatusBadRequest)
+ // Validate email format
+ if err := validators.ValidateEmail(req.Email); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -140,8 +138,8 @@ func CheckEmailHandler(w http.ResponseWriter, r *http.Request) {
return
}
- if _, err := mail.ParseAddress(email); err != nil {
- http.Error(w, "invalid email format", http.StatusBadRequest)
+ if err := validators.ValidateEmail(email); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
return
}
diff --git a/backend/internal/validators/email.go b/backend/internal/validators/email.go
new file mode 100644
index 0000000..54a6cb5
--- /dev/null
+++ b/backend/internal/validators/email.go
@@ -0,0 +1,40 @@
+package validators
+
+import (
+ "errors"
+ "net/mail"
+ "regexp"
+ "strings"
+)
+
+var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
+
+// ValidateEmail checks that an email is non-empty, within length limits,
+// and conforms to both RFC 5322 (via net/mail) and a basic regex shape.
+func ValidateEmail(email string) error {
+ email = strings.TrimSpace(email)
+ if email == "" {
+ return errors.New("email is required")
+ }
+ if len(email) > 254 {
+ return errors.New("email exceeds maximum length of 254 characters")
+ }
+ if _, err := mail.ParseAddress(email); err != nil {
+ return errors.New("invalid email format")
+ }
+ if !emailRegex.MatchString(email) {
+ return errors.New("invalid email format")
+ }
+ return nil
+}
+
+// NormalizeGiftCardCode strips non-alphanumeric characters and upper-cases the code.
+func NormalizeGiftCardCode(code string) string {
+ clean := ""
+ for _, char := range code {
+ if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') {
+ clean += string(char)
+ }
+ }
+ return clean
+}
diff --git a/backend/internal/validators/email_test.go b/backend/internal/validators/email_test.go
new file mode 100644
index 0000000..e256b8b
--- /dev/null
+++ b/backend/internal/validators/email_test.go
@@ -0,0 +1,157 @@
+//go:build test
+// +build test
+
+package validators
+
+import (
+ "testing"
+)
+
+func TestValidateEmail_Valid(t *testing.T) {
+ valid := []string{
+ "user@example.com",
+ "user.name+tag@example.co.uk",
+ "a@b.cd",
+ "test@sub.example.org",
+ "123@example.com",
+ }
+ for _, e := range valid {
+ if err := ValidateEmail(e); err != nil {
+ t.Errorf("expected valid email %q, got error: %v", e, err)
+ }
+ }
+}
+
+func TestValidateEmail_Invalid(t *testing.T) {
+ invalid := []string{
+ "",
+ "not-an-email",
+ "@example.com",
+ "user@",
+ "user@.com",
+ "user@example",
+ "a@b.c", // TLD too short
+ }
+ for _, e := range invalid {
+ if err := ValidateEmail(e); err == nil {
+ t.Errorf("expected invalid email %q to return error", e)
+ }
+ }
+}
+
+func TestValidateEmail_TooLong(t *testing.T) {
+ long := string(make([]byte, 255))
+ if err := ValidateEmail(long); err == nil {
+ t.Error("expected error for email exceeding 254 chars")
+ }
+}
+
+func TestValidateEmail_WhitespaceTrimmed(t *testing.T) {
+ // ValidateEmail trims whitespace, so leading/trailing spaces are acceptable.
+ if err := ValidateEmail(" user@example.com "); err != nil {
+ t.Errorf("expected valid after trimming whitespace, got: %v", err)
+ }
+}
+
+func TestNormalizeGiftCardCode_StripsNonAlphanumeric(t *testing.T) {
+ result := NormalizeGiftCardCode("abc-123_xyz!@#")
+ if result != "abc123xyz" {
+ t.Errorf("expected abc123xyz, got %s", result)
+ }
+}
+
+func TestNormalizeGiftCardCode_PreservesCase(t *testing.T) {
+ result := NormalizeGiftCardCode("aBcDeF123456")
+ if result != "aBcDeF123456" {
+ t.Errorf("expected aBcDeF123456, got %s", result)
+ }
+}
+
+func TestNormalizeGiftCardCode_Empty(t *testing.T) {
+ result := NormalizeGiftCardCode("")
+ if result != "" {
+ t.Errorf("expected empty string, got %s", result)
+ }
+}
+
+func TestNormalizeGiftCardCode_AlreadyClean(t *testing.T) {
+ result := NormalizeGiftCardCode("ABCDEF123456")
+ if result != "ABCDEF123456" {
+ t.Errorf("expected ABCDEF123456, got %s", result)
+ }
+}
+
+func TestValidateEmail_RejectsSQLInjection(t *testing.T) {
+ payloads := []string{
+ "' OR '1'='1",
+ "admin'--",
+ "'; DROP TABLE users;--",
+ `" OR 1=1 --`,
+ "' OR '1'='1' --",
+ "1' OR '1'='1",
+ "' UNION SELECT * FROM users --",
+ "admin'/*",
+ }
+ for _, p := range payloads {
+ if err := ValidateEmail(p); err == nil {
+ t.Errorf("expected SQLi payload %q to be rejected", p)
+ }
+ }
+}
+
+func TestValidateEmail_RejectsXSS(t *testing.T) {
+ payloads := []string{
+ "",
+ "
",
+ "\">",
+ "javascript:alert(1)",
+ }
+ for _, p := range payloads {
+ if err := ValidateEmail(p); err == nil {
+ t.Errorf("expected XSS payload %q to be rejected", p)
+ }
+ }
+}
+
+func TestValidateEmail_RejectsCommandInjection(t *testing.T) {
+ payloads := []string{
+ "; rm -rf /",
+ "| cat /etc/passwd",
+ "`id`",
+ "$(cat /etc/passwd)",
+ }
+ for _, p := range payloads {
+ if err := ValidateEmail(p); err == nil {
+ t.Errorf("expected command injection payload %q to be rejected", p)
+ }
+ }
+}
+
+func TestValidateEmail_RejectsControlChars(t *testing.T) {
+ payloads := []string{
+ "user@example.com\nX-Injected: header",
+ "user@example.com\r\nX-Injected: header",
+ "user\x00@example.com",
+ "user@ex\tample.com",
+ }
+ for _, p := range payloads {
+ if err := ValidateEmail(p); err == nil {
+ t.Errorf("expected control char payload %q to be rejected", p)
+ }
+ }
+}
+
+func TestValidateEmail_ValidMailsAreSafe(t *testing.T) {
+ // These are perfectly valid emails that happen to contain
+ // characters used in injection attacks — verify they pass.
+ valid := []string{
+ "safe.sql+select@example.com",
+ "safe.xss+script@example.co.uk",
+ "drop+table@example.org",
+ }
+ for _, e := range valid {
+ if err := ValidateEmail(e); err != nil {
+ t.Errorf("expected safe email %q to be valid, got: %v", e, err)
+ }
+ }
+}