From 689309a4526fd696aa458d1edc9876a6ba363ac3 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Sat, 30 May 2026 00:32:03 +0000 Subject: [PATCH 1/2] test(audit): add CEF injection escaping regression tests (PILOT-263) Two failing tests that demonstrate the vulnerability: - Action containing pipe characters corrupts CEF header parsing, injecting fake extension fields (cn2, src, act). - Details containing pipe and newline characters corrupt msg extension and create fake CEF lines. --- audit/zz_audit_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/audit/zz_audit_test.go b/audit/zz_audit_test.go index 1f14593..d6a7040 100644 --- a/audit/zz_audit_test.go +++ b/audit/zz_audit_test.go @@ -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() From 3b7ecd5fa07bfff7da12ad28563d623511934965 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Sat, 30 May 2026 00:32:53 +0000 Subject: [PATCH 2/2] fix(audit): CEF-escape Action and Details to prevent log injection (PILOT-263) User-controlled Action and Details strings were interpolated directly into CEF output without escaping. An attacker could inject | and = characters to forge extension fields or fake CEF headers consumed by SIEM parsers. Add cefEscape() helper that escapes \, =, |, \r, \n per the CEF escaping convention, and apply it to Action before both the header and extensions, and to Details before the msg extension. Closes PILOT-263 --- audit/audit_export.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/audit/audit_export.go b/audit/audit_export.go index 076cc15..cf91f35 100644 --- a/audit/audit_export.go +++ b/audit/audit_export.go @@ -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|||| func (ae *AuditExporter) formatCEF(entry *Entry) ([]byte, error) { @@ -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 }