From de4430ea5d6f8605120985039f7087e9cb2a5310 Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 16 Sep 2025 21:31:27 -0700 Subject: [PATCH] feat: Introduce allowed_domains for web widget (#12450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We wanted to provide an option for users to specify the domains on which they can show the website. The rest of the sites shouldn't see the widget at all. It's not possible generally through Origin because you can't get Origin when loading via an iframe. What I've done is add frame ancestors for the domains specified in allowed domains. I hope this solves most of the problems. This is added in a way that it won't affect existing widgets. Only If they have configured allowed domains, it will start blocking. Otherwise, it would follow the previous behavior without any changes. This change supports called wild card domains as well. You can add a comma‑separated list of domains, either wild card or regular domains. --- To test, deploy to staging. Call the following API to update the allowed_domains list. ``` URL: PATCH /api/v1/accounts//inboxes/ Payload: { "channel": { "allowed_domains": "*.chatwoot.dev,chatwoot.com" } } ``` Fixes https://github.com/chatwoot/chatwoot/issues/1985 --- app/controllers/widgets_controller.rb | 7 ++++++- app/models/channel/web_widget.rb | 3 ++- app/views/api/v1/models/_inbox.json.jbuilder | 1 + ...0250916024703_add_allowed_domains_to_channel_widgets.rb | 5 +++++ db/schema.rb | 3 ++- 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20250916024703_add_allowed_domains_to_channel_widgets.rb diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 4be690ffe..9a6a376f7 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -70,7 +70,12 @@ class WidgetsController < ActionController::Base end def allow_iframe_requests - response.headers.delete('X-Frame-Options') + if @web_widget.allowed_domains.blank? + response.headers.delete('X-Frame-Options') + else + domains = @web_widget.allowed_domains.split(',').map(&:strip).join(' ') + response.headers['Content-Security-Policy'] = "frame-ancestors #{domains}" + end end end diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb index 9e3016eac..d4e9989c1 100644 --- a/app/models/channel/web_widget.rb +++ b/app/models/channel/web_widget.rb @@ -3,6 +3,7 @@ # Table name: channel_web_widgets # # id :integer not null, primary key +# allowed_domains :text default("") # continuity_via_email :boolean default(TRUE), not null # feature_flags :integer default(7), not null # hmac_mandatory :boolean default(FALSE) @@ -31,7 +32,7 @@ class Channel::WebWidget < ApplicationRecord self.table_name = 'channel_web_widgets' EDITABLE_ATTRS = [:website_url, :widget_color, :welcome_title, :welcome_tagline, :reply_time, :pre_chat_form_enabled, - :continuity_via_email, :hmac_mandatory, + :continuity_via_email, :hmac_mandatory, :allowed_domains, { pre_chat_form_options: [:pre_chat_message, :require_email, { pre_chat_fields: [:field_type, :label, :placeholder, :name, :enabled, :type, :enabled, :required, diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 1b2ab177b..d9134d563 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -33,6 +33,7 @@ end json.tweets_enabled resource.channel.try(:tweets_enabled) if resource.twitter? ## WebWidget Attributes +json.allowed_domains resource.channel.try(:allowed_domains) json.widget_color resource.channel.try(:widget_color) json.website_url resource.channel.try(:website_url) json.hmac_mandatory resource.channel.try(:hmac_mandatory) diff --git a/db/migrate/20250916024703_add_allowed_domains_to_channel_widgets.rb b/db/migrate/20250916024703_add_allowed_domains_to_channel_widgets.rb new file mode 100644 index 000000000..b0df78df6 --- /dev/null +++ b/db/migrate/20250916024703_add_allowed_domains_to_channel_widgets.rb @@ -0,0 +1,5 @@ +class AddAllowedDomainsToChannelWidgets < ActiveRecord::Migration[7.1] + def change + add_column :channel_web_widgets, :allowed_domains, :text, default: '' + end +end diff --git a/db/schema.rb b/db/schema.rb index b762891a3..36a532e86 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_08_26_000000) do +ActiveRecord::Schema[7.1].define(version: 2025_09_16_024703) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -533,6 +533,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_26_000000) do t.jsonb "pre_chat_form_options", default: {} t.boolean "hmac_mandatory", default: false t.boolean "continuity_via_email", default: true, null: false + t.text "allowed_domains", default: "" t.index ["hmac_token"], name: "index_channel_web_widgets_on_hmac_token", unique: true t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true end