Redact PII from Square webhook logging

payment.updated/refund.updated handlers log only the Square object id and payload length instead of the raw JSON body (which contained buyer email, card brand/last4, cardholder name). Add a test asserting no raw payload reaches the log.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 515b828550
commit 2652aa66be
2 changed files with 54 additions and 2 deletions
+18 -2
View File
@@ -145,10 +145,26 @@ func verifySquareSignature(body []byte, signature, signingKey, notificationURL s
return hmac.Equal([]byte(signature), []byte(expected))
}
// handlePaymentUpdated logs only the Square object id — never the raw payload,
// which contains PII (buyer email, card brand/last4, cardholder name, billing
// address, amounts). The envelope's event_id is logged at the dispatch site.
// On unmarshal failure log just the byte length (no content).
func handlePaymentUpdated(data json.RawMessage) {
log.Printf("[SQUARE-WEBHOOK] payment.updated: %s", string(data))
var obj struct{ ID string `json:"id"` }
if err := json.Unmarshal(data, &obj); err != nil {
log.Printf("[SQUARE-WEBHOOK] payment.updated received (payload length=%d)", len(data))
return
}
log.Printf("[SQUARE-WEBHOOK] payment.updated received (data.id=%s)", obj.ID)
}
// handleRefundUpdated logs only the Square object id — never the raw payload,
// which contains PII. See handlePaymentUpdated.
func handleRefundUpdated(data json.RawMessage) {
log.Printf("[SQUARE-WEBHOOK] refund.updated: %s", string(data))
var obj struct{ ID string `json:"id"` }
if err := json.Unmarshal(data, &obj); err != nil {
log.Printf("[SQUARE-WEBHOOK] refund.updated received (payload length=%d)", len(data))
return
}
log.Printf("[SQUARE-WEBHOOK] refund.updated received (data.id=%s)", obj.ID)
}
@@ -10,6 +10,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"strings"
@@ -264,6 +265,41 @@ func TestHandleSquareWebhook_RejectedWhenKeyEmpty(t *testing.T) {
}
}
func TestHandleSquareWebhook_NoRawPayloadInLogs(t *testing.T) {
event := SquareWebhookEvent{
Type: "payment.updated",
EventID: "evt_pii_1",
CreatedAt: "2025-01-01T00:00:00Z",
Data: json.RawMessage(`{"id":"payment_pii_1","buyer_email_address":"secret@example.com",
"card_details":{"card":{"brand":"VISA","last_4":"1234","cardholder_name":"Jane Doe"}}}`),
}
body, _ := json.Marshal(event)
sig := webhookTestEnv(t, body)
var buf bytes.Buffer
oldOutput := log.Writer()
log.SetOutput(&buf)
defer log.SetOutput(oldOutput)
w := makeWebhookRequest(body, sig, context.Background())
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
out := buf.String()
for _, pii := range []string{"secret@example.com", "VISA", "1234", "Jane Doe", "payment_pii_1_full"} {
if strings.Contains(out, pii) {
t.Errorf("log output leaked PII %q:\n%s", pii, out)
}
}
if !strings.Contains(out, "data.id=payment_pii_1") {
t.Errorf("expected log to include object id, got:\n%s", out)
}
if !strings.Contains(out, "Received event: payment.updated") {
t.Errorf("expected log to reference event type, got:\n%s", out)
}
}
// =============================================================================
// Dedup — event_id replay protection
// =============================================================================