fix: apply shared conversation protection to scalar logs and named controls

This commit is contained in:
2026-10-10 01:36:41 +08:00
parent 2bfe4ec7e8
commit 02e6ab9380
3 changed files with 67 additions and 2 deletions
+3
View File
@@ -185,6 +185,9 @@ func redactErrorJSON(s string) string {
func cleanString(s string) string {
s = privatePEMInText.ReplaceAllString(s, redacted)
// Scalar messages/errors and HTTP text share the same labelled-content
// protection, rather than protecting conversational blocks only in bodies.
s = redactConversationLines(redactMarkup(s))
s = redactErrorJSON(s)
s = authHeaderInText.ReplaceAllStringFunc(s, func(header string) string {
index := strings.IndexAny(header, ":=")
+26 -2
View File
@@ -58,7 +58,10 @@ func redactMarkup(body string) string {
}
}
out.WriteString(tag)
if kind == html.StartTagToken && sensitive(token.Data) {
// A named sensitive field owns both its value attribute and enclosed
// content (textarea/select/custom controls). Void inputs own only value;
// they must never swallow following non-sensitive business elements.
if kind == html.StartTagToken && (sensitive(token.Data) || (hideValue && !voidMarkupElement(token.Data))) {
out.WriteString(redacted)
hiddenTag = token.Data
depth = 1
@@ -70,6 +73,15 @@ func redactMarkup(body string) string {
return out.String()
}
func voidMarkupElement(name string) bool {
switch name {
case "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr":
return true
default:
return false
}
}
// A form can be identified without guessing semantic values. Untouched pairs
// preserve their original encoding and order, including duplicate field names.
func redactForm(body string) (string, bool) {
@@ -104,6 +116,16 @@ func redactForm(body string) (string, bool) {
var textFieldLine = regexp.MustCompile(`^\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*[:=]`)
func conversationField(key string) bool {
key = strings.ToLower(strings.NewReplacer("_", "", "-", "", ".", "").Replace(key))
switch key {
case "prompt", "systemprompt", "systemmessage", "instructions", "messages", "transcript", "transcription", "text", "content", "audio", "closingremark", "conversation":
return true
default:
return false
}
}
func redactConversationLines(body string) string {
lines := strings.SplitAfter(body, "\n")
hiding := false
@@ -129,7 +151,9 @@ func redactConversationLines(body string) string {
if strings.HasSuffix(line, "\n") {
lines[i] += "\n"
}
hiding = true
// Credentials/header fields own their single line, not unrelated
// following diagnostics. Only conversational fields own continuation.
hiding = conversationField(field[1])
}
}
return strings.Join(lines, "")
+38
View File
@@ -11,6 +11,44 @@ import (
"testing"
)
func TestAssociatedSensitiveControlsHideContentWithoutSwallowingBusiness(t *testing.T) {
body := `<form><input name="password" value="private-password"><p>business-before</p><textarea name="prompt">private-full-prompt</textarea><select name="messages"><option>private-full-dialogue</option></select><div name="transcript">private-transcript</div><p>business-after</p></form>`
safe, err := RedactText([]byte(body))
if err != nil {
t.Fatal(err)
}
for _, secret := range []string{"private-password", "private-full-prompt", "private-full-dialogue", "private-transcript"} {
if strings.Contains(safe, secret) {
t.Fatalf("leak %s: %s", secret, safe)
}
}
for _, keep := range []string{"business-before", "business-after"} {
if !strings.Contains(safe, keep) {
t.Fatal("non-sensitive markup lost", safe)
}
}
}
func TestFreeTextConversationIsHiddenInBothModesAndAllScalarPaths(t *testing.T) {
value := "upstream failed\nmessages:\nuser: private-user-turn\nassistant: private-assistant-turn\n\nstatus=retry_pending\nattempts=2"
for _, debug := range []bool{false, true} {
out := installTestLogger(t, debug)
slog.Error("operation failed", "error", errors.New(value), "detail", value)
slog.Info(value, "stage", "failure_observed")
text := out.String()
for _, secret := range []string{"private-user-turn", "private-assistant-turn"} {
if strings.Contains(text, secret) {
t.Fatalf("leak in mode %v: %s", debug, text)
}
}
for _, keep := range []string{"upstream failed", "retry_pending", "attempts=2"} {
if !strings.Contains(text, keep) {
t.Fatal("failure metadata lost", text)
}
}
}
}
func TestFreeTextAuthorizationAndUnlabelledPrivatePEMAreHidden(t *testing.T) {
for _, debug := range []bool{false, true} {
out := installTestLogger(t, debug)