Skip to content

Commit 496b8f6

Browse files
feat: enhance WhatsApp interface with new tools, security hardening, and tests
- Rewrite WhatsAppTools with 9 sync-only tools and enable/disable flags - Add send_reply_buttons, send_list_message, send_image, send_document, send_location, send_reaction, and mark_as_read tools - Harden security: remove dev mode bypass, add replay protection (5-min window) - Handle interactive replies, location, reaction, contacts, sticker messages - Replace requests with httpx in utils - Add Pydantic response models and operation IDs to router - Add 48 unit tests covering tools, security, and router
1 parent d2e7813 commit 496b8f6

File tree

8 files changed

+1334
-414
lines changed

8 files changed

+1334
-414
lines changed

libs/agno/agno/os/interfaces/whatsapp/router.py

Lines changed: 156 additions & 100 deletions
Large diffs are not rendered by default.
Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,44 @@
11
import hashlib
22
import hmac
33
import os
4+
import time
45
from typing import Optional
56

67
from agno.utils.log import log_warning
78

89

9-
def is_development_mode() -> bool:
10-
"""Check if the application is running in development mode."""
11-
return os.getenv("APP_ENV", "development").lower() == "development"
12-
13-
1410
def get_app_secret() -> str:
15-
"""
16-
Get the WhatsApp app secret from environment variables.
17-
In development mode, returns a dummy secret if WHATSAPP_APP_SECRET is not set.
18-
"""
1911
app_secret = os.getenv("WHATSAPP_APP_SECRET")
20-
2112
if not app_secret:
22-
raise ValueError("WHATSAPP_APP_SECRET environment variable is not set in production mode")
23-
13+
raise ValueError("WHATSAPP_APP_SECRET environment variable is not set")
2414
return app_secret
2515

2616

27-
def validate_webhook_signature(payload: bytes, signature_header: Optional[str]) -> bool:
28-
"""
29-
Validate the webhook payload using SHA256 signature.
30-
In development mode, signature validation can be bypassed.
17+
def validate_webhook_signature(
18+
payload: bytes, signature_header: Optional[str], timestamp: Optional[int] = None
19+
) -> bool:
20+
"""Validate the webhook payload using SHA256 signature.
3121
3222
Args:
3323
payload: The raw request payload bytes
3424
signature_header: The X-Hub-Signature-256 header value
25+
timestamp: Optional Unix timestamp from the message for replay protection
3526
3627
Returns:
37-
bool: True if signature is valid or in development mode, False otherwise
28+
bool: True if signature is valid and timestamp is within 5-minute window
3829
"""
39-
# In development mode, we can bypass signature validation
40-
if is_development_mode():
41-
log_warning("Bypassing signature validation in development mode")
42-
return True
30+
if timestamp is not None:
31+
if abs(time.time() - timestamp) > 300:
32+
log_warning("Rejecting webhook: timestamp too old (possible replay attack)")
33+
return False
4334

4435
if not signature_header or not signature_header.startswith("sha256="):
4536
return False
4637

4738
app_secret = get_app_secret()
4839
expected_signature = signature_header.split("sha256=")[1]
4940

50-
# Calculate signature
5141
hmac_obj = hmac.new(app_secret.encode("utf-8"), payload, hashlib.sha256)
5242
calculated_signature = hmac_obj.hexdigest()
5343

54-
# Compare signatures using constant-time comparison
5544
return hmac.compare_digest(calculated_signature, expected_signature)

0 commit comments

Comments
 (0)