mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:21:29 +00:00
security: reject CRLF/NUL in email headers to prevent SMTP injection (fixes H-3)
H-3 in certctl-audit-report.md: caller-supplied From/To/Subject were
interpolated directly into the SMTP DATA payload and handed to
client.Mail / client.Rcpt with no sanitization, allowing an attacker
who controls any of those values to inject extra headers (Bcc:,
Reply-To:), split the message body (CRLFCRLF), or tamper with the
SMTP envelope. CWE-113.
Fix:
- New package helper internal/validation.ValidateHeaderValue(field,
value). Rejects CR ("\r"), LF ("\n"), and NUL ("\x00") with an error
that names the offending field but does NOT echo the raw value,
so log readers cannot be attacked with injected content. Silent
stripping was considered and rejected: authentication-relevant
headers must fail visibly.
- Two-layer defense in internal/connector/notifier/email/email.go:
(1) primary guard at the top of sendEmail / sendHTMLEmail, which
blocks tampering of the SMTP envelope (client.Mail, client.Rcpt)
since net/smtp does not sanitize those arguments; and
(2) defense-in-depth guard inside formatEmailMessage /
formatHTMLEmailMessage, catching any future caller that
bypasses sendEmail. Both format functions now return an error.
- Body content is intentionally NOT validated — CR/LF in body is legal
RFC 5322 content and net/smtp handles dot-stuffing.
Tests:
- internal/validation/headers_test.go: 3 functions (AcceptsSafeInput,
RejectsControlCharacters, DefaultFieldName) covering plain ASCII,
UTF-8 multibyte, tabs, typical email addresses, CRLF injection,
lone CR, lone LF, NUL, CRLFCRLF body split, trailing CR, leading LF.
Each reject case asserts the field name IS in the error and the
raw offending value IS NOT (anti-log-injection).
- internal/connector/notifier/email/email_test.go: added
TestEmail_FormatEmailMessage_RejectsCRLFInjection and
TestEmail_FormatHTMLEmailMessage_RejectsCRLFInjection. Existing
format tests updated for the new (bytes, error) signature.
Wire-format invariants preserved:
- SMTP DATA headers still use CRLF separators and RFC 1123Z Date
(unchanged).
- Content-Type headers unchanged (text/plain for plain, text/html +
MIME-Version: 1.0 for HTML).
- No change to message encoding or transport.
Verification (Go 1.25.9 linux-arm64, parent 0750c5f):
- go build ./... clean
- go vet ./... clean
- go test -race ./internal/validation/... ok
- go test -race ./internal/connector/notifier/email/... ok
- go test -race ./internal/connector/notifier/webhook/... ok
- Per-layer coverage gates all pass:
validation 95.1% (+0.7 vs baseline 94.4%)
email 39.7% (+1.4 vs baseline 38.3%)
service 67.8% (unchanged)
handler 78.6% (unchanged)
middleware 80.0% (unchanged)
domain 92.7% (unchanged)
- govulncheck ./... No vulnerabilities found
- golangci-lint run ./internal/validation/... ./internal/connector/notifier/email/...
0 issues
Operational note: SMTP sends that would previously deliver a
tampered message now fail fast at the notifier with a clear error.
Operators who were relying on header-injection-shaped inputs (there
should be none in practice — all callers are internal certctl code)
will see "failed to format message: <field> contains disallowed
control character" in logs.
Scope: H-3 only. H-4 (webhook SSRF) follows in a separate commit.
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shankar0123/certctl/internal/connector/notifier"
|
"github.com/shankar0123/certctl/internal/connector/notifier"
|
||||||
|
"github.com/shankar0123/certctl/internal/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config represents the email notifier configuration.
|
// Config represents the email notifier configuration.
|
||||||
@@ -123,7 +124,22 @@ func (c *Connector) SendEvent(ctx context.Context, event notifier.Event) error {
|
|||||||
|
|
||||||
// sendEmail sends an email message using the configured SMTP server.
|
// sendEmail sends an email message using the configured SMTP server.
|
||||||
// It handles both TLS and plain authentication modes.
|
// It handles both TLS and plain authentication modes.
|
||||||
|
//
|
||||||
|
// Header values (From, To, Subject) are validated up-front to reject CR, LF,
|
||||||
|
// and NUL characters. This blocks SMTP header injection (CWE-113) and also
|
||||||
|
// prevents injection into the SMTP envelope commands MAIL FROM and RCPT TO,
|
||||||
|
// since net/smtp does not sanitize those inputs itself.
|
||||||
func (c *Connector) sendEmail(ctx context.Context, to, subject, body string) error {
|
func (c *Connector) sendEmail(ctx context.Context, to, subject, body string) error {
|
||||||
|
if err := validation.ValidateHeaderValue("From", c.config.FromAddress); err != nil {
|
||||||
|
return fmt.Errorf("invalid sender: %w", err)
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("To", to); err != nil {
|
||||||
|
return fmt.Errorf("invalid recipient: %w", err)
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("Subject", subject); err != nil {
|
||||||
|
return fmt.Errorf("invalid subject: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
addr := net.JoinHostPort(c.config.SMTPHost, strconv.Itoa(c.config.SMTPPort))
|
addr := net.JoinHostPort(c.config.SMTPHost, strconv.Itoa(c.config.SMTPPort))
|
||||||
|
|
||||||
// Connect to SMTP server
|
// Connect to SMTP server
|
||||||
@@ -182,8 +198,13 @@ func (c *Connector) sendEmail(ctx context.Context, to, subject, body string) err
|
|||||||
}
|
}
|
||||||
defer wc.Close()
|
defer wc.Close()
|
||||||
|
|
||||||
// Format and write email headers and body
|
// Format and write email headers and body. The format function
|
||||||
message := c.formatEmailMessage(c.config.FromAddress, to, subject, body)
|
// re-validates header values as defense-in-depth; the early-return
|
||||||
|
// above should have already caught any injection attempt.
|
||||||
|
message, err := c.formatEmailMessage(c.config.FromAddress, to, subject, body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to format message: %w", err)
|
||||||
|
}
|
||||||
if _, err := wc.Write(message); err != nil {
|
if _, err := wc.Write(message); err != nil {
|
||||||
return fmt.Errorf("failed to write message: %w", err)
|
return fmt.Errorf("failed to write message: %w", err)
|
||||||
}
|
}
|
||||||
@@ -197,7 +218,22 @@ func (c *Connector) sendEmail(ctx context.Context, to, subject, body string) err
|
|||||||
|
|
||||||
// sendHTMLEmail sends an HTML email message using the configured SMTP server.
|
// sendHTMLEmail sends an HTML email message using the configured SMTP server.
|
||||||
// Used by the digest service for rich HTML digest emails.
|
// Used by the digest service for rich HTML digest emails.
|
||||||
|
//
|
||||||
|
// Header values (From, To, Subject) are validated up-front to reject CR, LF,
|
||||||
|
// and NUL characters. This blocks SMTP header injection (CWE-113) and also
|
||||||
|
// prevents injection into the SMTP envelope commands MAIL FROM and RCPT TO,
|
||||||
|
// since net/smtp does not sanitize those inputs itself.
|
||||||
func (c *Connector) sendHTMLEmail(ctx context.Context, to, subject, htmlBody string) error {
|
func (c *Connector) sendHTMLEmail(ctx context.Context, to, subject, htmlBody string) error {
|
||||||
|
if err := validation.ValidateHeaderValue("From", c.config.FromAddress); err != nil {
|
||||||
|
return fmt.Errorf("invalid sender: %w", err)
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("To", to); err != nil {
|
||||||
|
return fmt.Errorf("invalid recipient: %w", err)
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("Subject", subject); err != nil {
|
||||||
|
return fmt.Errorf("invalid subject: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
addr := net.JoinHostPort(c.config.SMTPHost, strconv.Itoa(c.config.SMTPPort))
|
addr := net.JoinHostPort(c.config.SMTPHost, strconv.Itoa(c.config.SMTPPort))
|
||||||
|
|
||||||
var auth smtp.Auth
|
var auth smtp.Auth
|
||||||
@@ -250,7 +286,12 @@ func (c *Connector) sendHTMLEmail(ctx context.Context, to, subject, htmlBody str
|
|||||||
}
|
}
|
||||||
defer wc.Close()
|
defer wc.Close()
|
||||||
|
|
||||||
message := c.formatHTMLEmailMessage(c.config.FromAddress, to, subject, htmlBody)
|
// The format function re-validates header values as defense-in-depth;
|
||||||
|
// the early-return above should have already caught any injection attempt.
|
||||||
|
message, err := c.formatHTMLEmailMessage(c.config.FromAddress, to, subject, htmlBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to format message: %w", err)
|
||||||
|
}
|
||||||
if _, err := wc.Write(message); err != nil {
|
if _, err := wc.Write(message); err != nil {
|
||||||
return fmt.Errorf("failed to write message: %w", err)
|
return fmt.Errorf("failed to write message: %w", err)
|
||||||
}
|
}
|
||||||
@@ -263,7 +304,20 @@ func (c *Connector) sendHTMLEmail(ctx context.Context, to, subject, htmlBody str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// formatEmailMessage formats an email message with standard headers.
|
// formatEmailMessage formats an email message with standard headers.
|
||||||
func (c *Connector) formatEmailMessage(from, to, subject, body string) []byte {
|
// It rejects any header value containing CR, LF, or NUL bytes to prevent
|
||||||
|
// SMTP header injection (CWE-113). See internal/validation.ValidateHeaderValue.
|
||||||
|
// The body is not validated — CR/LF in the body is legitimate content, and
|
||||||
|
// SMTP dot-stuffing / length framing are handled by net/smtp.
|
||||||
|
func (c *Connector) formatEmailMessage(from, to, subject, body string) ([]byte, error) {
|
||||||
|
if err := validation.ValidateHeaderValue("From", from); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("To", to); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("Subject", subject); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
message := fmt.Sprintf(
|
message := fmt.Sprintf(
|
||||||
"From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n%s",
|
"From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n%s",
|
||||||
from,
|
from,
|
||||||
@@ -272,11 +326,24 @@ func (c *Connector) formatEmailMessage(from, to, subject, body string) []byte {
|
|||||||
time.Now().Format(time.RFC1123Z),
|
time.Now().Format(time.RFC1123Z),
|
||||||
body,
|
body,
|
||||||
)
|
)
|
||||||
return []byte(message)
|
return []byte(message), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatHTMLEmailMessage formats an HTML email message with MIME headers.
|
// formatHTMLEmailMessage formats an HTML email message with MIME headers.
|
||||||
func (c *Connector) formatHTMLEmailMessage(from, to, subject, htmlBody string) []byte {
|
// It rejects any header value containing CR, LF, or NUL bytes to prevent
|
||||||
|
// SMTP header injection (CWE-113). See internal/validation.ValidateHeaderValue.
|
||||||
|
// The HTML body is not validated at this layer — CR/LF in HTML content is
|
||||||
|
// legitimate, and SMTP dot-stuffing / length framing are handled by net/smtp.
|
||||||
|
func (c *Connector) formatHTMLEmailMessage(from, to, subject, htmlBody string) ([]byte, error) {
|
||||||
|
if err := validation.ValidateHeaderValue("From", from); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("To", to); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validation.ValidateHeaderValue("Subject", subject); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
message := fmt.Sprintf(
|
message := fmt.Sprintf(
|
||||||
"From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\n\r\n%s",
|
"From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\n\r\n%s",
|
||||||
from,
|
from,
|
||||||
@@ -285,7 +352,7 @@ func (c *Connector) formatHTMLEmailMessage(from, to, subject, htmlBody string) [
|
|||||||
time.Now().Format(time.RFC1123Z),
|
time.Now().Format(time.RFC1123Z),
|
||||||
htmlBody,
|
htmlBody,
|
||||||
)
|
)
|
||||||
return []byte(message)
|
return []byte(message), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatAlertBody formats an alert notification as email body text.
|
// formatAlertBody formats an alert notification as email body text.
|
||||||
|
|||||||
@@ -138,7 +138,10 @@ func TestEmail_FormatMessage_RFC822Headers(t *testing.T) {
|
|||||||
subject := "Test Subject"
|
subject := "Test Subject"
|
||||||
body := "Test Body"
|
body := "Test Body"
|
||||||
|
|
||||||
message := conn.formatEmailMessage(from, to, subject, body)
|
message, err := conn.formatEmailMessage(from, to, subject, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected nil error, got %v", err)
|
||||||
|
}
|
||||||
messageStr := string(message)
|
messageStr := string(message)
|
||||||
|
|
||||||
if !strings.Contains(messageStr, "From: "+from) {
|
if !strings.Contains(messageStr, "From: "+from) {
|
||||||
@@ -177,7 +180,10 @@ func TestEmail_FormatHTMLEmailMessage_Headers(t *testing.T) {
|
|||||||
subject := "HTML Test"
|
subject := "HTML Test"
|
||||||
htmlBody := "<html><body><h1>Test</h1></body></html>"
|
htmlBody := "<html><body><h1>Test</h1></body></html>"
|
||||||
|
|
||||||
message := conn.formatHTMLEmailMessage(from, to, subject, htmlBody)
|
message, err := conn.formatHTMLEmailMessage(from, to, subject, htmlBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected nil error, got %v", err)
|
||||||
|
}
|
||||||
messageStr := string(message)
|
messageStr := string(message)
|
||||||
|
|
||||||
if !strings.Contains(messageStr, "From: "+from) {
|
if !strings.Contains(messageStr, "From: "+from) {
|
||||||
@@ -200,6 +206,67 @@ func TestEmail_FormatHTMLEmailMessage_Headers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestEmail_FormatEmailMessage_RejectsCRLFInjection exercises the CRLF
|
||||||
|
// sanitizer (CWE-113). A subject containing "\r\nBcc: ..." must be rejected
|
||||||
|
// rather than silently stripped — authentication-relevant headers are
|
||||||
|
// security-critical and silent mutation masks malicious intent.
|
||||||
|
func TestEmail_FormatEmailMessage_RejectsCRLFInjection(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SMTPHost: "smtp.example.com",
|
||||||
|
SMTPPort: 587,
|
||||||
|
FromAddress: "sender@example.com",
|
||||||
|
}
|
||||||
|
logger := newTestLogger()
|
||||||
|
conn := New(cfg, logger)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
from, to, sub string
|
||||||
|
wantField string
|
||||||
|
}{
|
||||||
|
{"CRLF in Subject", "sender@example.com", "recipient@example.com", "hello\r\nBcc: attacker@example.com", "Subject"},
|
||||||
|
{"LF in To", "sender@example.com", "recipient@example.com\nBcc: x@y", "ok", "To"},
|
||||||
|
{"CR in From", "sender@example.com\rExtra: header", "recipient@example.com", "ok", "From"},
|
||||||
|
{"NUL in Subject", "sender@example.com", "recipient@example.com", "hi\x00there", "Subject"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := conn.formatEmailMessage(tc.from, tc.to, tc.sub, "body")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected injection error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.wantField) {
|
||||||
|
t.Errorf("expected error to mention field %q, got %q", tc.wantField, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEmail_FormatHTMLEmailMessage_RejectsCRLFInjection mirrors the plain-text
|
||||||
|
// test for the HTML codepath used by the digest service.
|
||||||
|
func TestEmail_FormatHTMLEmailMessage_RejectsCRLFInjection(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SMTPHost: "smtp.example.com",
|
||||||
|
SMTPPort: 587,
|
||||||
|
FromAddress: "sender@example.com",
|
||||||
|
}
|
||||||
|
logger := newTestLogger()
|
||||||
|
conn := New(cfg, logger)
|
||||||
|
|
||||||
|
_, err := conn.formatHTMLEmailMessage(
|
||||||
|
"sender@example.com",
|
||||||
|
"recipient@example.com",
|
||||||
|
"digest\r\nBcc: attacker@example.com",
|
||||||
|
"<p>hi</p>",
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected CRLF injection error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Subject") {
|
||||||
|
t.Errorf("expected error to mention Subject field, got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEmail_FormatAlertBody(t *testing.T) {
|
func TestEmail_FormatAlertBody(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
SMTPHost: "smtp.example.com",
|
SMTPHost: "smtp.example.com",
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateHeaderValue rejects any value that contains characters capable of
|
||||||
|
// breaking out of a header line and injecting additional headers or body
|
||||||
|
// content. It guards against CRLF injection (CWE-113) in RFC 5322 message
|
||||||
|
// headers (SMTP, IMAP, etc.) and RFC 7230 HTTP headers alike.
|
||||||
|
//
|
||||||
|
// Disallowed characters:
|
||||||
|
// - Carriage return ("\r")
|
||||||
|
// - Line feed ("\n")
|
||||||
|
// - NUL ("\x00")
|
||||||
|
//
|
||||||
|
// The field name is included in the returned error solely for operator
|
||||||
|
// diagnostics; the offending value is not echoed back, so untrusted input
|
||||||
|
// does not leak into logs that render this error.
|
||||||
|
//
|
||||||
|
// Callers should invoke this on any string that will be interpolated into a
|
||||||
|
// header (From, To, Subject, Reply-To, custom X-* headers, etc.) before the
|
||||||
|
// headers are serialized. Values containing CR/LF/NUL MUST be rejected
|
||||||
|
// outright; silent stripping is inappropriate for authentication-relevant
|
||||||
|
// headers because it can mask malicious intent while still altering the
|
||||||
|
// message.
|
||||||
|
func ValidateHeaderValue(field, value string) error {
|
||||||
|
if field == "" {
|
||||||
|
field = "header"
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(value, "\r\n\x00") {
|
||||||
|
return fmt.Errorf("%s contains disallowed control character (CR, LF, or NUL)", field)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateHeaderValue_AcceptsSafeInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
field string
|
||||||
|
value string
|
||||||
|
}{
|
||||||
|
{"plain ASCII", "Subject", "Renewal reminder"},
|
||||||
|
{"empty string", "Reply-To", ""},
|
||||||
|
{"utf-8 multibyte", "Subject", "résumé — 日本語"},
|
||||||
|
{"tabs and spaces permitted", "Subject", "a\tb c"},
|
||||||
|
{"typical email address", "From", "alerts@example.com"},
|
||||||
|
{"long Subject within limits", "Subject", strings.Repeat("x", 998)},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if err := ValidateHeaderValue(tc.field, tc.value); err != nil {
|
||||||
|
t.Fatalf("expected nil error, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateHeaderValue_RejectsControlCharacters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
field string
|
||||||
|
value string
|
||||||
|
}{
|
||||||
|
{"injected CRLF + header", "Subject", "hello\r\nBcc: attacker@example.com"},
|
||||||
|
{"lone LF", "From", "alice@example.com\nBcc: x@y"},
|
||||||
|
{"lone CR", "Subject", "hello\rworld"},
|
||||||
|
{"NUL byte", "To", "bob@example.com\x00extra"},
|
||||||
|
{"CRLFCRLF body injection", "Subject", "ping\r\n\r\nMalicious body"},
|
||||||
|
{"CR at end", "Subject", "trailing\r"},
|
||||||
|
{"LF at start", "Subject", "\nleading"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := ValidateHeaderValue(tc.field, tc.value)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error rejecting control characters, got nil")
|
||||||
|
}
|
||||||
|
// Error must mention the field so operators can pinpoint the offender.
|
||||||
|
if !strings.Contains(err.Error(), tc.field) {
|
||||||
|
t.Errorf("expected error to mention field %q, got %q", tc.field, err.Error())
|
||||||
|
}
|
||||||
|
// Error must NOT leak the raw value back into logs.
|
||||||
|
if strings.Contains(err.Error(), tc.value) {
|
||||||
|
t.Errorf("error leaks raw value; expected redaction: %q", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateHeaderValue_DefaultFieldName(t *testing.T) {
|
||||||
|
err := ValidateHeaderValue("", "bad\r\nvalue")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for CRLF input, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "header") {
|
||||||
|
t.Errorf("expected default field name 'header' in error, got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user