From b006f5249296ffcfe12dd6ad3b82fef6800aade9 Mon Sep 17 00:00:00 2001 From: Junjun Zhang Date: Fri, 2 Oct 2026 22:09:17 +0800 Subject: [PATCH] fix(security): detect ENCRYPTED PRIVATE KEY variant; gate truncated-PEM mask on base64 body (#3084) Co-Authored-By: ggcode --- internal/security/display_redact.go | 5 ++- internal/security/secretdetect.go | 5 ++- internal/security/zz_issue3084_test.go | 47 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 internal/security/zz_issue3084_test.go diff --git a/internal/security/display_redact.go b/internal/security/display_redact.go index 9f4c15c02..7b0378840 100644 --- a/internal/security/display_redact.go +++ b/internal/security/display_redact.go @@ -53,7 +53,10 @@ var displaySecretPatterns = []struct { // the 4th detection/display drift: the header (and any partial key body // behind it) rendered raw. Runs AFTER the complete-block pattern, so // only genuinely unterminated headers reach it; masks to end of buffer. - {"private_key_truncated", regexp.MustCompile(`(?s)-----BEGIN (?:[A-Z ]*)PRIVATE KEY-----.+`), []string{"-----BEGIN"}}, + // #3084: gated on a base64-looking body right after the header, so + // prose that merely MENTIONS the literal header (docs / teaching text) + // is no longer masked to end-of-buffer. + {"private_key_truncated", regexp.MustCompile(`(?s)(-----BEGIN (?:[A-Z ]*)PRIVATE KEY-----[\r\n]+[A-Za-z0-9+/=]{20,}.*)`), []string{"-----BEGIN"}}, {"openai_key", regexp.MustCompile(`\b(sk-(?:proj-|svcacct-)?[A-Za-z0-9\-_]{20,})\b`), []string{"sk-"}}, {"anthropic_key", regexp.MustCompile(`\b(sk-ant-[A-Za-z0-9\-_]{70,})\b`), []string{"sk-ant-"}}, {"jwt", regexp.MustCompile(`\b(eyJ[A-Za-z0-9_\-]{10,}\.eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,})\b`), []string{"eyJ"}}, diff --git a/internal/security/secretdetect.go b/internal/security/secretdetect.go index 1c7d6861b..630ed66e1 100644 --- a/internal/security/secretdetect.go +++ b/internal/security/secretdetect.go @@ -117,7 +117,10 @@ var secretPatterns = []SecretPattern{ ID: "private_key_block", Name: "Private Key (PEM)", Severity: "high", - Pattern: regexp.MustCompile(`-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----`), + // #3084: ENCRYPTED (openssl pkcs8 -topk8 passphrase form) added to + // the prefix alternation so the detection layer stops trailing the + // display layer's [A-Z ]* prefix class for this variant. + Pattern: regexp.MustCompile(`-----BEGIN (?:ENCRYPTED |RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----`), }, { ID: "ssh_private_key", diff --git a/internal/security/zz_issue3084_test.go b/internal/security/zz_issue3084_test.go new file mode 100644 index 000000000..a15481ede --- /dev/null +++ b/internal/security/zz_issue3084_test.go @@ -0,0 +1,47 @@ +package security + +import ( + "strings" + "testing" +) + +// #3084 V2: detection layer must flag the ENCRYPTED (passphrase-protected) +// private key form, matching the display layer's coverage. +func TestIssue3084EncryptedPEMDetected(t *testing.T) { + const body = "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKYwgg\n-----END ENCRYPTED PRIVATE KEY-----" + findings := ScanForSecrets("config/prod.key", body) + found := false + for _, f := range findings { + if f.PatternID == "private_key_block" { + found = true + } + } + if !found { + t.Errorf("ENCRYPTED PRIVATE KEY not flagged by private_key_block; findings=%v", findings) + } + // plain forms still detected (no regression) + if ScanForSecrets("config/k.key", "-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----") == nil { + t.Error("RSA PRIVATE KEY regression") + } +} + +// #3084 V1: prose that merely MENTIONS the literal header must not be +// masked to end-of-buffer; a genuine truncated key body still is. +func TestIssue3084TruncatedPEMGate(t *testing.T) { + // prose: header followed by ordinary explanation text (no base64 run) + prose := "-----BEGIN PRIVATE KEY-----\nThe PEM format wraps a base64 body between header and footer lines, as documented here." + if out := RedactForDisplay(prose); out != prose { + t.Errorf("prose mention masked: %q", out) + } + // genuine truncated key: header + 20+ base64 chars, no END + truncated := "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASC" + out := RedactForDisplay(truncated) + if strings.Contains(out, "MIIEvQIBADANBgkqhkiG9w0BAQEFAASC") { + t.Errorf("truncated key body leaked: %q", out) + } + // complete block still masked by the original pattern + complete := "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASC\n-----END PRIVATE KEY-----" + if out2 := RedactForDisplay(complete); strings.Contains(out2, "MIIEvQIBADANBgkqhkiG9w0BAQEFAASC") { + t.Errorf("complete block body leaked: %q", out2) + } +}