Module: Parse::Webhooks::ReplayProtection

Defined in:
lib/parse/webhooks/replay_protection.rb

Overview

NEW-EXT-4: webhook freshness and replay protection.

Parse Server’s default webhook delivery is authenticated only by the static X-Parse-Webhook-Key header. A captured POST is therefore indefinitely replayable – a Ruby-initiated save bearing an RB request id will continue to suppress server-side after_* callbacks every time it is replayed, and a generic trigger payload can be delivered repeatedly to fire double-charges or other side effects.

This module adds two layers on top of the existing static-key check:

  1. **Always-on body+request-id dedup.** A bounded LRU records a SHA-256 of (request_id || “”) joined with the request body. A duplicate seen within replay_window_seconds is rejected with “Webhook replay detected.”. Cooperation with Parse Server is not required; this protects against in-window replays only, but those are the cheapest attack to mount (proxy retries, captured fast loops, retransmits).

  2. **Opt-in HMAC freshness verification.** When a signing_secret is configured (programmatically or via PARSE_WEBHOOK_SIGNING_SECRET) the dispatcher requires two extra headers on every request:

    • X-Parse-Webhook-Timestamp – decimal Unix epoch seconds.

    • X-Parse-Webhook-Signature – hex-encoded HMAC-SHA256 of the bytes “#{timestamp}.#{body}” keyed with the signing secret.

    Requests outside signing_max_skew_seconds (default 300) or with an invalid signature are rejected. Once enabled, this gives full binding between the body and the time of delivery and closes the replay window beyond the freshness skew.

Operators wanting layer 2 must arrange for Parse Server to add these headers. Parse Server does not natively sign webhook deliveries, so this is typically done with a thin Cloud Code wrapper or an egress proxy. Until enabled, layer 1 still applies.

Defined Under Namespace

Classes: LruCache

Class Attribute Summary collapse

Class Attribute Details

.replay_cache_sizeObject

Maximum number of entries retained in the dedup LRU. Older entries are evicted to keep memory bounded.



87
88
89
# File 'lib/parse/webhooks/replay_protection.rb', line 87

def replay_cache_size
  @replay_cache_size || DEFAULT_REPLAY_CACHE_SIZE
end

.replay_window_secondsObject

How long a (request_id, body) digest stays in the dedup cache. Duplicates seen within this window are rejected.



81
82
83
# File 'lib/parse/webhooks/replay_protection.rb', line 81

def replay_window_seconds
  @replay_window_seconds || DEFAULT_REPLAY_WINDOW
end

.signing_max_skew_secondsObject

Maximum allowed clock skew (in seconds) between the timestamp header and the receiver. Requests outside this window are rejected as stale when signing_secret is set.



75
76
77
# File 'lib/parse/webhooks/replay_protection.rb', line 75

def signing_max_skew_seconds
  @signing_max_skew_seconds || DEFAULT_MAX_SKEW
end

.signing_secretObject

Shared HMAC secret used to verify X-Parse-Webhook-Signature. When nil/empty, signature verification is skipped (layer 1 still applies). Defaults to ENV.



67
68
69
70
# File 'lib/parse/webhooks/replay_protection.rb', line 67

def signing_secret
  return @signing_secret if defined?(@signing_secret) && !@signing_secret.nil?
  ENV["PARSE_WEBHOOK_SIGNING_SECRET"]
end