Files
chatwoot/config/integration/apps.yml
T
27404b4a27 fix: redact sensitive integration secrets from API responses (#14147)
## Summary

The `GET /api/v1/accounts/:id/integrations/apps` endpoint returns raw
secret values (OpenAI API keys, Google service account private keys,
Linear refresh tokens, etc.) in hook settings within the JSON response.
Although gated behind an administrator check, these secrets are visible
in the browser network tab.

This PR filters hook settings through the existing `visible_properties`
whitelist defined in `config/integration/apps.yml`, and adds explicit
whitelists to integrations that were missing them.

Closes #14042

## Bug reproduction

**Setup:** Created an OpenAI integration hook with a fake API key
(`sk-test-secret-12345`).

**Step 1 — Browser Network tab shows raw secrets:**

<img width="1676" height="869" alt="Screenshot 2026-04-24 at 14 32 28"
src="https://github.com/user-attachments/assets/6fc69463-365e-458b-b216-98a7390bf528"
/>


Navigate to Settings > Integrations as an admin. Open DevTools Network
tab and observe the response from `GET
/api/v1/accounts/:id/integrations/apps`. The hook settings contain the
full API key in plaintext:

```json
"hooks": [
  {
    "id": 1,
    "app_id": "openai",
    "settings": {
      "api_key": "sk-test-secret-12345",
      "label_suggestion": false
    }
  }
]
```

**Step 2 — API call confirms the leak:**

```bash
curl -s "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "access-token: <token>" \
  -H "client: <client>" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
```

Response:

```json
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "api_key": "sk-test-secret-12345",
        "label_suggestion": false
      }
    }
  ]
}
```

Any admin user can extract the raw API key from the response. The same
applies to other integrations — Dialogflow exposes full Google service
account credentials, Linear exposes refresh tokens, etc.

## After fix

After applying this change, the GET
/api/v1/accounts/:id/integrations/apps endpoint no longer returns
sensitive secret values in hook settings. Instead, the response is
filtered using each integration’s visible_properties whitelist, ensuring
only safe, user-facing fields are exposed. For example, OpenAI
integrations return non-sensitive fields like label_suggestion while
excluding raw API keys. This prevents secrets from being exposed in the
browser network tab or API responses, even for authenticated admin
users.

```bash
curl -X GET "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer eyJhY2Nlc3MtdG9rZW4iOiJqZG5jOWg4TnljaWFJa3JlZkxGQzRnIiwidG9rZW4tdHlwZSI6IkJlYXJlciIsImNsaWVudCI6Ik81bjVnTDFEOVVhbGpwbWxjaHZNanciLCJleHBpcnkiOiIxNzgyMzMyNTA4IiwidWlkIjoidXNlckB0ZXN0LmNvbSJ9" \
  -H "access-token: jdnc9h8NyciaIkrefLFC4g" \
  -H "client: O5n5gL1D9UaljpmlchvMjw" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10174  100 10174    0     0  33232      0 --:--:-- --:--:-- --:--:-- 33357
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "label_suggestion": false
      }
    }
  ]
}
```

## What changed

- Added `visible_properties` accessor to `Integrations::App` model to
expose the whitelist from config
- Updated `_hook.json.jbuilder` to filter `resource.settings` through
the associated app's `visible_properties` instead of returning the full
hash
- Added `visible_properties` to integrations that were missing it:
  - Linear: `[]` (settings contain refresh_token)
  - Notion: `[]` (OAuth-based, no user-facing settings)
- Slack: `['channel_name']` (UI needs this to display connected channel)
  - Shopify: `[]` (no settings)

App config metadata (`_app.json.jbuilder`) is left unchanged —
hook_type, settings_form_schema, etc. are not secrets and the frontend
depends on them.

## How to test

1. Create an OpenAI integration hook with an API key
2. As an admin, call `GET /api/v1/accounts/:id/integrations/apps`
3. Verify hook settings include `api_key` (whitelisted) but not raw
credential objects
4. For Dialogflow hooks, verify `credentials` (private key JSON) is
excluded while `project_id` is included
5. For Slack hooks, verify `channel_name` is still returned
6. For Linear hooks, verify `refresh_token` is not returned

---------

Co-authored-by: Botshelo Nokoane (Konstruktors) <botshelo@entersekt.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-14 06:53:13 +05:30

324 lines
10 KiB
YAML

###### Attributes Supported by Integration Apps #######
# id: Internal Id for the integrations, used by the hooks
# logo: place the image in /public/dashboard/images/integrations and reference here
# i18n_key: the key under which translations for the integration is placed in en.yml
# action: if integration requires external redirect url
# hook_type: ( account / inbox )
# feature_flag: (string) feature flag to enable/disable the integration
# allow_multiple_hooks: whether multiple hooks can be created for the integration
# visible_properties: hook setting keys safe to return in API responses and show in the UI
# settings_json_schema: the json schema used to validate the settings hash (https://json-schema.org/)
# settings_form_schema: the formulate schema used in frontend to render settings form (https://vueformulate.com/)
########################################################
webhooks:
id: webhook
logo: webhooks.png
i18n_key: webhooks
action: /webhook
hook_type: account
allow_multiple_hooks: true
dashboard_apps:
id: dashboard_apps
logo: dashboard_apps.png
i18n_key: dashboard_apps
hook_type: account
allow_multiple_hooks: true
openai:
id: openai
logo: openai.png
i18n_key: openai
action: /openai
hook_type: account
allow_multiple_hooks: false
settings_json_schema:
{
'type': 'object',
'properties':
{
'api_key': { 'type': 'string' },
'label_suggestion': { 'type': 'boolean' },
},
'required': ['api_key'],
'additionalProperties': false,
}
settings_form_schema:
[
{
'label': 'API Key',
'type': 'text',
'name': 'api_key',
'validation': 'required',
},
{
'label': 'Show label suggestions',
'type': 'checkbox',
'name': 'label_suggestion',
'validation': '',
},
]
visible_properties: ['label_suggestion']
linear:
id: linear
logo: linear.png
i18n_key: linear
action: https://linear.app/oauth/authorize
hook_type: account
allow_multiple_hooks: false
visible_properties: []
notion:
id: notion
logo: notion.png
i18n_key: notion
hook_type: account
allow_multiple_hooks: false
visible_properties: []
slack:
id: slack
logo: slack.png
i18n_key: slack
action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:read,channels:manage,channels:join,groups:read,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history,files:read,files:write
hook_type: account
allow_multiple_hooks: false
visible_properties: ['channel_name']
dialogflow:
id: dialogflow
logo: dialogflow.png
i18n_key: dialogflow
action: /dialogflow
hook_type: inbox
allow_multiple_hooks: true
settings_json_schema:
{
'type': 'object',
'properties':
{
'project_id': { 'type': 'string' },
'credentials': { 'type': 'object' },
'region': { 'type': 'string' },
'language_code': { 'type': 'string' },
},
'required': ['project_id', 'credentials'],
'additionalProperties': false,
}
settings_form_schema:
[
{
'label': 'Dialogflow Project ID',
'type': 'text',
'name': 'project_id',
'validation': 'required',
'validationName': 'Project Id',
},
{
'label': 'Dialogflow Project Key File',
'type': 'textarea',
'name': 'credentials',
'validation': 'required|JSON',
'validationName': 'Credentials',
'validation-messages':
{ 'JSON': 'Invalid JSON', 'required': 'Credentials is required' },
},
{
'label': 'Dialogflow Region',
'type': 'select',
'name': 'region',
'default': 'global',
'options': [
{ 'label': 'Global - Default', 'value': 'global' },
{ 'label': 'AS-NE1 - Tokyo, Japan', 'value': 'asia-northeast1' },
{ 'label': 'AU-SE1 - Sydney, Australia', 'value': 'australia-southeast1' },
{ 'label': 'EU-W1 - St. Ghislain, Belgium', 'value': 'europe-west1' },
{ 'label': 'EU-W2 - London, England', 'value': 'europe-west2' },
],
},
{
'label': 'Language Code',
'type': 'select',
'name': 'language_code',
'default': 'en-US',
'help': 'Language code for Dialogflow agent. Use "auto" to detect from contact language.',
'options': [
{ 'label': 'Auto-detect from contact', 'value': 'auto' },
{ 'label': 'English (US)', 'value': 'en-US' },
{ 'label': 'English (UK)', 'value': 'en-GB' },
{ 'label': 'Spanish (Spain)', 'value': 'es-ES' },
{ 'label': 'Spanish (Latin America)', 'value': 'es-419' },
{ 'label': 'French', 'value': 'fr-FR' },
{ 'label': 'German', 'value': 'de-DE' },
{ 'label': 'Portuguese (Brazil)', 'value': 'pt-BR' },
{ 'label': 'Portuguese (Portugal)', 'value': 'pt-PT' },
{ 'label': 'Italian', 'value': 'it-IT' },
{ 'label': 'Japanese', 'value': 'ja-JP' },
{ 'label': 'Korean', 'value': 'ko-KR' },
{ 'label': 'Chinese (Simplified)', 'value': 'zh-CN' },
{ 'label': 'Chinese (Traditional)', 'value': 'zh-TW' },
{ 'label': 'Hindi', 'value': 'hi-IN' },
{ 'label': 'Arabic', 'value': 'ar' },
{ 'label': 'Russian', 'value': 'ru-RU' },
{ 'label': 'Dutch', 'value': 'nl-NL' },
{ 'label': 'Polish', 'value': 'pl-PL' },
{ 'label': 'Turkish', 'value': 'tr-TR' },
{ 'label': 'Thai', 'value': 'th-TH' },
{ 'label': 'Vietnamese', 'value': 'vi-VN' },
{ 'label': 'Indonesian', 'value': 'id-ID' },
],
},
]
visible_properties: ['project_id', 'region', 'language_code']
google_translate:
id: google_translate
logo: google-translate.png
i18n_key: google_translate
action: /google_translate
hook_type: account
allow_multiple_hooks: false
settings_json_schema:
{
'type': 'object',
'properties':
{
'project_id': { 'type': 'string' },
'credentials': { 'type': 'object' },
},
'required': ['project_id', 'credentials'],
'additionalProperties': false,
}
settings_form_schema:
[
{
'label': 'Google Cloud Project ID',
'type': 'text',
'name': 'project_id',
'validation': 'required',
'validationName': 'Project Id',
},
{
'label': 'Google Cloud Project Key File',
'type': 'textarea',
'name': 'credentials',
'validation': 'required|JSON',
'validationName': 'Credentials',
'validation-messages':
{ 'JSON': 'Invalid JSON', 'required': 'Credentials is required' },
},
]
visible_properties: ['project_id']
dyte:
id: dyte
logo: dyte.png
i18n_key: dyte
action: /dyte
hook_type: account
allow_multiple_hooks: false
settings_json_schema:
{
'type': 'object',
'properties':
{
'api_key': { 'type': 'string' },
'organization_id': { 'type': 'string' },
},
'required': ['api_key', 'organization_id'],
'additionalProperties': false,
}
settings_form_schema:
[
{
'label': 'Organization ID',
'type': 'text',
'name': 'organization_id',
'validation': 'required',
},
{
'label': 'API Key',
'type': 'text',
'name': 'api_key',
'validation': 'required',
},
]
visible_properties: ['organization_id']
shopify:
id: shopify
logo: shopify.png
i18n_key: shopify
hook_type: account
allow_multiple_hooks: false
visible_properties: []
leadsquared:
id: leadsquared
feature_flag: crm_integration
logo: leadsquared.png
i18n_key: leadsquared
action: /leadsquared
hook_type: account
allow_multiple_hooks: false
settings_json_schema:
{
'type': 'object',
'properties':
{
'access_key': { 'type': 'string' },
'secret_key': { 'type': 'string' },
'endpoint_url': { 'type': 'string' },
'app_url': { 'type': 'string' },
'timezone': { 'type': 'string' },
'enable_conversation_activity': { 'type': 'boolean' },
'enable_transcript_activity': { 'type': 'boolean' },
'conversation_activity_score': { 'type': 'string' },
'transcript_activity_score': { 'type': 'string' },
'conversation_activity_code': { 'type': 'integer' },
'transcript_activity_code': { 'type': 'integer' },
},
'required': ['access_key', 'secret_key'],
'additionalProperties': false,
}
settings_form_schema:
[
{
'label': 'Access Key',
'type': 'text',
'name': 'access_key',
'validation': 'required',
},
{
'label': 'Secret Key',
'type': 'text',
'name': 'secret_key',
'validation': 'required',
},
{
'label': 'Push Conversation Activity',
'type': 'checkbox',
'name': 'enable_conversation_activity',
'help': 'Enable this option to push an activity when a conversation is created',
},
{
'label': 'Conversation Activity Score',
'type': 'number',
'name': 'conversation_activity_score',
'help': 'Score to assign to the conversation created activity, default is 0',
},
{
'label': 'Push Transcript Activity',
'type': 'checkbox',
'name': 'enable_transcript_activity',
'help': 'Enable this option to push an activity when a transcript is created',
},
{
'label': 'Transcript Activity Score',
'type': 'number',
'name': 'transcript_activity_score',
'help': 'Score to assign to the conversation transcript activity, default is 0',
},
]
visible_properties:
[
'endpoint_url',
'enable_conversation_activity',
'enable_transcript_activity',
'conversation_activity_score',
'transcript_activity_score',
]