hermes - ✅(Solved) Fix webhook: invalid signatures consume the authenticated rate-limit bucket [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#12544Fetched 2026-04-20 12:18:24
View on GitHub
Comments
0
Participants
1
Timeline
6
Reactions
0
Participants
Timeline (top)
referenced ×3cross-referenced ×2closed ×1

Webhook rate limiting is applied before HMAC validation. An attacker can spend the per-route quota with invalid signatures, causing the next valid signed delivery to receive 429 Rate limit exceeded.

Root Cause

Webhook rate limiting is applied before HMAC validation. An attacker can spend the per-route quota with invalid signatures, causing the next valid signed delivery to receive 429 Rate limit exceeded.

Fix Action

Fixed

PR fix notes

PR #12827: fix(webhook): validate HMAC signature before rate limiting

Description (problem / solution / changelog)

What changed

The webhook handler now validates HMAC signatures BEFORE applying rate limiting. Previously, the order was reversed — rate limiting ran first, meaning invalid (unauthenticated) requests could consume rate limit quota.

Before: An attacker could send unlimited invalid webhook requests, each consuming rate limit budget, potentially blocking legitimate webhooks.

After: Invalid signatures are rejected immediately with HTTP 401 without touching the rate limiter. Only validly-signed requests proceed to rate limiting. This follows the standard security practice of authenticating before authorizing.

How to test

  1. Configure a webhook endpoint with a signing secret
  2. Send requests with invalid signatures and verify they receive 401 without consuming rate limit
  3. Send valid requests and verify rate limiting still applies normally
  4. Run the test suite:
    pytest tests/gateway/test_webhook_signature_rate_limit.py -v

Platforms tested

  • Linux (Docker container, Python 3.11)

Closes #12544

Security impact: This PR fixes a security issue — unauthenticated requests consuming rate limit budget.

Changed files

  • gateway/platforms/webhook.py (modified, +12/-12)
  • tests/gateway/test_webhook_signature_rate_limit.py (added, +289/-0)

PR #12843: fix(webhook): validate HMAC signature before rate limiting (#12544)

Description (problem / solution / changelog)

Salvage of #12827 by @Tranquil-Flow onto current main (was 10 commits behind). Clean cherry-pick, contributor authorship preserved.

Summary

Unauthenticated webhook requests no longer consume the per-route rate-limit bucket — HMAC signature check runs first, so invalid deliveries 401 without touching the 429 counter. Closes #12544.

Changes

  • gateway/platforms/webhook.py: swap the HMAC block above the rate-limit block in _handle_webhook(). Body-size check stays up front.
  • tests/gateway/test_webhook_signature_rate_limit.py: 3 scenarios — invalid sigs don't consume quota, valid sigs still rate-limit, mixed interleave behaves correctly.

Validation

BeforeAfter
5 bad-sig requests then 1 valid429202
3 valid-sig requests then 1 more429429 (unchanged)
tests/gateway/test_webhook_signature_rate_limit.py + test_webhook_adapter.py39 passed

Closes #12827.

Changed files

  • gateway/platforms/webhook.py (modified, +12/-12)
  • tests/gateway/test_webhook_signature_rate_limit.py (added, +289/-0)

Code Example

import hmac, hashlib, asyncio
from gateway.platforms.webhook import WebhookAdapter
from gateway.config import PlatformConfig

class FakeRequest:
    def __init__(self, body, signature):
        self.match_info = {'route_name': 'demo'}
        self.headers = {'X-Hub-Signature-256': signature}
        self.content_length = len(body)
        self.method = 'POST'
        self._body = body
    async def read(self):
        return self._body

secret = 'topsecret'
body = b'{"hello":"world"}'
adapter = WebhookAdapter(PlatformConfig(enabled=True, extra={
    'routes': {'demo': {'source': 'unit', 'secret': secret}},
    'rate_limit': 1,
}))

bad = 'sha256=' + '0'*64
good = 'sha256=' + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()

r1 = asyncio.run(adapter._handle_webhook(FakeRequest(body, bad)))
r2 = asyncio.run(adapter._handle_webhook(FakeRequest(body, good)))
print(r1.status, r2.status)
# Actual: 401 429
# Expected: 401 202
RAW_BUFFERClick to expand / collapse

Summary

Webhook rate limiting is applied before HMAC validation. An attacker can spend the per-route quota with invalid signatures, causing the next valid signed delivery to receive 429 Rate limit exceeded.

Affected code

  • gateway/platforms/webhook.py:298-324
  • tests/gateway/test_webhook_adapter.py (rate limiting and signature validation are covered separately, but not together)

Why this is a bug

_handle_webhook() appends to the fixed-window rate bucket before _validate_signature() runs. Invalid requests therefore consume the same quota as authenticated deliveries.

Minimal reproduction

import hmac, hashlib, asyncio
from gateway.platforms.webhook import WebhookAdapter
from gateway.config import PlatformConfig

class FakeRequest:
    def __init__(self, body, signature):
        self.match_info = {'route_name': 'demo'}
        self.headers = {'X-Hub-Signature-256': signature}
        self.content_length = len(body)
        self.method = 'POST'
        self._body = body
    async def read(self):
        return self._body

secret = 'topsecret'
body = b'{"hello":"world"}'
adapter = WebhookAdapter(PlatformConfig(enabled=True, extra={
    'routes': {'demo': {'source': 'unit', 'secret': secret}},
    'rate_limit': 1,
}))

bad = 'sha256=' + '0'*64
good = 'sha256=' + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()

r1 = asyncio.run(adapter._handle_webhook(FakeRequest(body, bad)))
r2 = asyncio.run(adapter._handle_webhook(FakeRequest(body, good)))
print(r1.status, r2.status)
# Actual: 401 429
# Expected: 401 202

Expected behavior

  • Invalid signatures should be rejected without consuming the authenticated request budget.

Actual behavior

  • A single bad-signature request can block the next valid request for the route.

Suggested investigation

  • Move the rate-limit increment after auth succeeds, or maintain separate unauthenticated throttling.
  • Add a regression test for the 401 -> 429 sequence.

extent analysis

TL;DR

Move the rate-limit increment after authentication succeeds to prevent invalid signatures from consuming the authenticated request budget.

Guidance

  • Investigate modifying the _handle_webhook() function in gateway/platforms/webhook.py to increment the rate limit after the _validate_signature() check, ensuring that only authenticated requests consume the quota.
  • Add a regression test to tests/gateway/test_webhook_adapter.py to cover the sequence of a 401 response followed by a 429 response, as described in the minimal reproduction example.
  • Consider maintaining separate unauthenticated throttling to prevent abuse while allowing valid requests to proceed.
  • Review the WebhookAdapter class to ensure that the rate limiting and signature validation are properly synchronized.

Example

# Pseudo-code example, actual implementation may vary
def _handle_webhook(self, request):
    # ...
    if self._validate_signature(request):
        # Increment rate limit after successful authentication
        self._increment_rate_limit()
        # Proceed with handling the request
    else:
        # Reject the request without incrementing the rate limit
        return 401

Notes

The provided minimal reproduction example demonstrates the issue, and the suggested investigation points towards a solution. However, the actual implementation details may vary depending on the specific requirements and constraints of the WebhookAdapter class.

Recommendation

Apply a workaround by moving the rate-limit increment after authentication succeeds, as this approach directly addresses the root cause of the issue and prevents invalid signatures from consuming the authenticated request budget.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

  • Invalid signatures should be rejected without consuming the authenticated request budget.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING