From 71fffdd2b91a405fbb70f35a7aa01c21f8862097 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Tue, 21 Jul 2026 04:39:29 +0530 Subject: [PATCH] fix: rate limit widget conversation transcript API (#15085) ## Description The widget conversation transcript endpoint (`POST /api/v1/widget/conversations/transcript`) has no rate limit. Every other comparable endpoint does: the agent-facing transcript API and the widget conversation-create and contact-update endpoints are all throttled. This gap lets a single client trigger a large burst of transcript emails from one conversation. This adds an IP-based throttle (5 requests/hour) for the endpoint, placed inside the existing widget-API throttle block so it inherits the `ENABLE_RACK_ATTACK_WIDGET_API` opt-out used by embedded/iframe clients. The limit is generous for legitimate use (a visitor emailing themselves a transcript) while stopping abusive loops. Throttled requests get the standard 429 the widget already handles. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? `config/initializers/rack_attack.rb` throttles have no existing specs in this file, so this follows the established convention (no new spec). Verified `ruby -c` and `rubocop` pass on the file. The new throttle mirrors the sibling widget throttles directly above it (same IP key, path guard, and structure). ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] My changes generate no new warnings --------- Co-authored-by: Sojan Jose --- config/initializers/rack_attack.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 41ccae971..caf49c047 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -31,10 +31,11 @@ class Rack::Attack (default_allowed_ips + env_allowed_ips).include?(remote_ip) end - # Rails would allow requests to paths with extensions, so lets compare against the path with extension stripped - # example /auth & /auth.json would both work + # Rails allows paths with extensions and trailing slashes, so compare against a normalized path. + # For example, /auth, /auth.json, and /auth/ should all use the same throttle. def path_without_extensions - path[/^[^.]+/] + normalized_path = path[/^[^.]+/] + normalized_path == '/' ? normalized_path : normalized_path.sub(%r{/+\z}, '') end end @@ -188,6 +189,11 @@ class Rack::Attack throttle('widget?website_token={website_token}&cw_conversation={x-auth-token}', limit: 5, period: 1.hour) do |req| req.ip if req.path_without_extensions == '/widget' && ActionDispatch::Request.new(req.env).params['cw_conversation'].blank? end + + ## Prevent Transcript Bombing on Widget API ### + throttle('api/v1/widget/conversations/transcript', limit: 5, period: 1.hour) do |req| + req.ip if req.path_without_extensions == '/api/v1/widget/conversations/transcript' && req.post? + end end ##-----------------------------------------------##