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:
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user