{"record":{"id":"b9532c1b7931ccc6","repo":"docusealco/docuseal","slug":"in-future","errorCode":null,"errorMessage":"In future","messagePattern":"In future","errorType":"exception","errorClass":"WebhookUrls::Signatures::TimestampError","httpStatus":null,"severity":"error","filePath":"lib/webhook_urls/signatures.rb","lineNumber":31,"sourceCode":"\n    def generate_secret\n      SECRET_PREFIX + Base64.strict_encode64(SecureRandom.bytes(SECRET_BYTES))\n    end\n\n    def sign(secret, body:, timestamp: Time.current.to_i)\n      \"#{timestamp}.#{OpenSSL::HMAC.hexdigest('sha256', secret, \"#{timestamp}.#{body}\")}\"\n    end\n\n    def verify(secret, body:, header:, tolerance: TOLERANCE)\n      ts, sig = header.to_s.split('.', 2)\n      ts = Integer(ts, exception: false)\n\n      raise InvalidSignatureError unless ts && sig\n\n      now = Time.current.to_i\n\n      raise TimestampError, 'Too old' if ts < now - tolerance\n      raise TimestampError, 'In future' if ts > now + tolerance\n\n      expected = OpenSSL::HMAC.hexdigest('sha256', secret, \"#{ts}.#{body}\")\n\n      raise InvalidSignatureError unless ActiveSupport::SecurityUtils.secure_compare(expected, sig)\n\n      true\n    end\n  end\nend\n","sourceCodeStart":13,"sourceCodeEnd":41,"githubUrl":"https://github.com/docusealco/docuseal/blob/004a22c1c88109c7ba0b567df011a8cb13894001/lib/webhook_urls/signatures.rb#L13-L41","documentation":"WebhookUrls::Signatures.verify (lib/webhook_urls/signatures.rb:22) parses the header as '<unix_timestamp>.<hmac>' and enforces a freshness window: the timestamp must be within `tolerance` seconds (default TOLERANCE = 300) of the receiver's clock. Line 31 raises TimestampError('In future') when ts > now + tolerance, i.e. the signature's timestamp is more than 5 minutes ahead of the verifying server. This blocks fabricated or replayed signatures that claim to be issued in the future, which usually indicates clock skew between signer and verifier rather than an attack.","triggerScenarios":"Calling WebhookUrls::Signatures.verify(secret, body:, header:) with a header whose timestamp component satisfies ts > Time.current.to_i + tolerance. Concretely: the sender machine's clock runs more than 5 minutes ahead of the receiver; sign was called with a millisecond epoch (13-digit ts, e.g. (Time.current.to_i * 1000) or JavaScript Date.now()) so ts is ~year 50000; a custom timestamp: passed to Signatures.sign that is in the future; or in tests, a header fixture generated earlier being verified against a frozen/traveled clock (Timecop) that is behind real time.","commonSituations":"NTP not running or drifted on Docker/Kubernetes nodes, VMs after host suspend, or bare-metal servers, so signer and verifier clocks diverge; porting sender code from JavaScript where Date.now() yields milliseconds; replaying recorded webhook fixtures in an automated test suite without re-signing them against the test clock; staging and production servers in different time sources; explicitly passing a future timestamp when generating test signatures.","solutions":["Compare clocks on both hosts (date -u on each, or log ts - Time.current.to_i when the error fires) and fix drift by enabling NTP/chrony/systemd-timesyncd on the machine whose clock is off.","If the sender uses milliseconds, switch to unix seconds: use Time.current.to_i (Ruby) or Math.floor(Date.now() / 1000) (JS) as the timestamp passed to WebhookUrls::Signatures.sign.","In tests, do not reuse recorded headers; regenerate the header inside the test with WebhookUrls::Signatures.sign(secret, body: body) while the clock is frozen, so signer and verifier share the same 'now'.","As a temporary measure while clock sync is being fixed, pass a larger window explicitly: WebhookUrls::Signatures.verify(secret, body:, header:, tolerance: 15 * 60) — shrink it back to 300 once skew is corrected, since a wide window weakens replay protection."],"exampleFix":"# before (sender signs with a millisecond epoch)\nheader = WebhookUrls::Signatures.sign(secret, body: payload, timestamp: Time.current.to_i * 1000)\n# -> verifier raises TimestampError 'In future' (ts is centuries ahead)\n\n# after (unix seconds on both sides)\nheader = WebhookUrls::Signatures.sign(secret, body: payload, timestamp: Time.current.to_i)","handlingStrategy":"try-catch","validationCode":"# Pre-check the timestamp window before calling verify, so a skewed clock\n# is reported as data, not as an exception:\nts, _sig = request.headers['Webhook-Signature'].to_s.split('.', 2)\nts = Integer(ts, exception: false)\nif ts && ts > Time.current.to_i + WebhookUrls::Signatures::TOLERANCE\n  Rails.logger.warn(\"webhook clock skew: sender is #{ts - Time.current.to_i}s ahead\")\n  return head :bad_request\nend","typeGuard":"def webhook_timestamp_plausible?(header, tolerance: WebhookUrls::Signatures::TOLERANCE)\n  ts, _sig = header.to_s.split('.', 2)\n  ts = Integer(ts, exception: false)\n  !!ts && ts.between?(Time.current.to_i - tolerance, Time.current.to_i + tolerance)\nend","tryCatchPattern":"begin\n  WebhookUrls::Signatures.verify(secret, body: request.raw_post, header: request.headers['Webhook-Signature'])\nrescue WebhookUrls::Signatures::TimestampError => e\n  # 'Too old' or 'In future': clock skew or replay. Respond 400 and log the\n  # delta (parsed ts vs now). Do NOT widen tolerance here or auto-retry —\n  # fix clock sync instead. Note: TimestampError is NOT a superclass of\n  # InvalidSignatureError; rescue them as separate clauses.\n  Rails.logger.warn(\"webhook timestamp rejected: #{e.message}\")\n  head :bad_request\nend","preventionTips":["Run NTP/chrony/systemd-timesyncd on every host that signs or verifies webhooks, and alert when sender-vs-receiver drift exceeds 60s.","Standardize on unix seconds for the signature timestamp across all senders (never milliseconds from Date.now()/Time#to_i * 1000).","In test suites, generate the signature inside the test with WebhookUrls::Signatures.sign under the same frozen clock instead of replaying recorded header fixtures.","Log the timestamp delta (ts - Time.current.to_i) on every verification; drift approaching the 300s tolerance is an early warning before failures start."],"tags":["webhook","hmac","timestamp","clock-skew","ruby","replay-protection"],"backgroundTag":"clock-skew","analyzedSha":"004a22c1c88109c7ba0b567df011a8cb13894001","analyzedAt":"2026-08-21T13:38:23.343Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}