From 81ec6f9bf50517f9cf620b18b5ec9e265b663e3b Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Wed, 11 Nov 2026 07:50:54 +0100 Subject: [PATCH] Request log: write & < > as sent (closes #327) Request lines of `waf` and `rproxy` now hold `&`, `<` and `>` as sent, where `json.Marshal` wrote `\u0026`, `\u003c` and `\u003e`, and are still valid JSON. `writeJSONLine` encodes with a `json.Encoder` that has HTML escaping off, and still writes each line in one write. This follows the `AGENTS.md` rule that the request log records query strings exactly as received, and lets `grep` find a logged query string. Alert webhooks and the report `rproxy` posts to `waf` are unchanged: neither is a request line, and both are read only by a JSON parser. Model: opus-5-5 --- internal/requestlog/requestlog.go | 13 +++++++-- internal/requestlog/requestlog_test.go | 38 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/internal/requestlog/requestlog.go b/internal/requestlog/requestlog.go index d5a98b7..8bdfda3 100644 --- a/internal/requestlog/requestlog.go +++ b/internal/requestlog/requestlog.go @@ -5,6 +5,7 @@ package requestlog import ( + "bytes" "encoding/json" "fmt" "io" @@ -252,14 +253,20 @@ func WriteRProxyLine(w io.Writer, line *RProxyLine) error { return writeJSONLine(w, line) } -// writeJSONLine writes line to w as one JSON line. +// writeJSONLine writes line to w as one JSON line. It writes "&", "<" and +// ">" as they are, so a query string reads as it was sent. func writeJSONLine(w io.Writer, line any) error { - encoded, err := json.Marshal(line) + var encoded bytes.Buffer + + encoder := json.NewEncoder(&encoded) + encoder.SetEscapeHTML(false) + + err := encoder.Encode(line) if err != nil { return fmt.Errorf("encode the request log line: %w", err) } - _, err = w.Write(append(encoded, '\n')) + _, err = w.Write(encoded.Bytes()) if err != nil { return fmt.Errorf("write the request log line: %w", err) } diff --git a/internal/requestlog/requestlog_test.go b/internal/requestlog/requestlog_test.go index 79c50df..764fb86 100644 --- a/internal/requestlog/requestlog_test.go +++ b/internal/requestlog/requestlog_test.go @@ -67,6 +67,44 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) { } } +func TestRequestLinesHoldTheQueryAndRefererAsSent(t *testing.T) { + t.Parallel() + + var out bytes.Buffer + + err := requestlog.Write(&out, &requestlog.Line{ + Query: "a=1&b=<2>", + Referer: "https://example.com/?x=1&y=<2>", + }) + if err != nil { + t.Fatalf("write: %v", err) + } + + err = requestlog.WriteRProxyLine(&out, &requestlog.RProxyLine{Query: "a=1&b=<2>"}) + if err != nil { + t.Fatalf("write the rproxy line: %v", err) + } + + lines := strings.Split(strings.TrimSuffix(out.String(), "\n"), "\n") + if len(lines) != 2 { + t.Fatalf("wrote %q, want two lines", out.String()) + } + + for _, line := range lines { + if !json.Valid([]byte(line)) { + t.Errorf("%s is not valid JSON", line) + } + + if !strings.Contains(line, `"query":"a=1&b=<2>"`) { + t.Errorf("%s does not hold the query as sent", line) + } + } + + if !strings.Contains(lines[0], `"referer":"https://example.com/?x=1&y=<2>"`) { + t.Errorf("%s does not hold the referer as sent", lines[0]) + } +} + func TestWriteRProxyLineWritesOneJSONLineWithOnlyItsFields(t *testing.T) { t.Parallel()