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))
|
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) {
|
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) {
|
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/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"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
|
// Dedup — event_id replay protection
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user