Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions audit/audit_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ func (ae *AuditExporter) formatSplunkHEC(entry *Entry) ([]byte, error) {
return json.Marshal(hec)
}

// cefEscape escapes |, =, \, \r, and \n characters as \|, \=, \\, \r, \n
// so that user-controlled fields cannot inject fake CEF headers or extensions.
func cefEscape(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "=", "\\=")
s = strings.ReplaceAll(s, "|", "\\|")
s = strings.ReplaceAll(s, "\r", "\\r")
s = strings.ReplaceAll(s, "\n", "\\n")
return s
}

// formatCEF produces a CEF (Common Event Format) line for SIEM ingestion.
// Format: CEF:0|Pilot|Registry|1.0|<action>|<action>|<severity>|<extensions>
func (ae *AuditExporter) formatCEF(entry *Entry) ([]byte, error) {
Expand All @@ -214,16 +225,18 @@ func (ae *AuditExporter) formatCEF(entry *Entry) ([]byte, error) {
severity = 4 // medium
}

safeAction := cefEscape(entry.Action)

extensions := fmt.Sprintf("dvc=pilot-registry dvchost=registry "+
"cs1=%s cs1Label=action cn1=%d cn1Label=network_id cn2=%d cn2Label=node_id",
entry.Action, entry.NetworkID, entry.NodeID)
safeAction, entry.NetworkID, entry.NodeID)

if entry.Details != "" {
extensions += fmt.Sprintf(" msg=%s", entry.Details)
extensions += fmt.Sprintf(" msg=%s", cefEscape(entry.Details))
}

line := fmt.Sprintf("CEF:0|Pilot|Registry|1.0|%s|%s|%d|%s",
entry.Action, entry.Action, severity, extensions)
safeAction, safeAction, severity, extensions)

return []byte(line), nil
}
Expand Down
41 changes: 41 additions & 0 deletions audit/zz_audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,47 @@ func TestAuditExporterExportBufferFullIncrementsDropped(t *testing.T) {
}
}

// TestFormatCEFEscapesInjectionCharactersInAction verifies that pipe, equals,
// backslash, and newline characters in Action are CEF-escaped so an attacker
// cannot inject fake header fields or extension key=value pairs.
func TestFormatCEFEscapesInjectionCharactersInAction(t *testing.T) {
t.Parallel()
ae := &AuditExporter{config: &wire.BlueprintAuditExport{Format: "syslog_cef"}}
action := "register|cn2=99|src=10.0.0.1|act=succeeded"
body, err := ae.formatCEF(&Entry{Action: action, NetworkID: 1, NodeID: 2})
if err != nil {
t.Fatalf("formatCEF: %v", err)
}
line := string(body)
// The raw pipe must not appear as a bare separator.
if strings.Contains(line, "|cn2=99") {
t.Fatalf("CEF output contains unescaped pipe in Action injection: %s", line)
}
// The escaped pipe should appear.
if !strings.Contains(line, `\|cn2`) {
t.Fatalf("CEF output missing escaped pipe in Action: %s", line)
}
}

// TestFormatCEFEscapesInjectionCharactersInDetails verifies that pipe and
// equals characters in Details are CEF-escaped.
func TestFormatCEFEscapesInjectionCharactersInDetails(t *testing.T) {
t.Parallel()
ae := &AuditExporter{config: &wire.BlueprintAuditExport{Format: "syslog_cef"}}
details := "error|msg=something bad\nline2"
body, err := ae.formatCEF(&Entry{Action: "register", NetworkID: 1, NodeID: 2, Details: details})
if err != nil {
t.Fatalf("formatCEF: %v", err)
}
line := string(body)
if strings.Contains(line, "|msg=") {
t.Fatalf("Details contains unescaped pipe: %s", line)
}
if strings.Contains(line, "\nline2") {
t.Fatalf("Details contains unescaped newline: %s", line)
}
}

// TestSendUnknownFormatUsesJSON verifies the default JSON fallback.
func TestSendUnknownFormatUsesJSON(t *testing.T) {
t.Parallel()
Expand Down
Loading