From fbcb89e955ec920aa40a5da2e6d606650c576f00 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 14 May 2026 18:52:14 +0530 Subject: [PATCH] fix(swagger): prevent path traversal in docs controller (#14458) This hardens the development/test Swagger docs endpoint by ensuring requested files are resolved only within the `swagger/` directory. This did not affect production security because the Swagger controller only renders files in development or test environments; production already returns `404`. The change still closes the scanner finding and prevents future automated reports from flagging the development-only path. ## Closes Addresses: GHSA-xhp7-ggjq-p2rg ## How to reproduce 1. Start Chatwoot locally in development. 2. Visit `/swagger/%2Fetc%2Fpasswd`. 3. Before this change, the endpoint could render files outside the Swagger directory in development/test. ## What changed - Resolve Swagger file requests relative to `Rails.root/swagger`. - Return `404` when the resolved path is outside the Swagger directory or does not point to a file. - Strip leading slashes from derived request paths. - Add a request spec for the encoded absolute-path case. ## How to test 1. Start the app locally. 2. Visit `/swagger` and confirm the ReDoc page loads. 3. Visit `/swagger/swagger.json` and confirm the Swagger JSON loads. 4. Visit `/swagger/%2Fetc%2Fpasswd` and confirm it returns `404` with no file contents. Note: `bundle exec rspec spec/controllers/swagger_controller_spec.rb` was passing locally earlier during this fix. A final rerun before opening the PR was blocked because local Postgres on `localhost:5432` was not accepting connections. Co-authored-by: Muhsin Keloth --- app/controllers/swagger_controller.rb | 11 ++++++++--- spec/controllers/swagger_controller_spec.rb | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/controllers/swagger_controller.rb b/app/controllers/swagger_controller.rb index af5a4b039..680bf92fc 100644 --- a/app/controllers/swagger_controller.rb +++ b/app/controllers/swagger_controller.rb @@ -1,7 +1,12 @@ class SwaggerController < ApplicationController def respond if Rails.env.development? || Rails.env.test? - render inline: Rails.root.join('swagger', derived_path).read + swagger_root = Rails.root.join('swagger') + file_path = swagger_root.join(derived_path).cleanpath + + return head :not_found unless file_path.to_s.start_with?("#{swagger_root}/") && file_path.file? + + render inline: file_path.read else head :not_found end @@ -11,8 +16,8 @@ class SwaggerController < ApplicationController def derived_path params[:path] ||= 'index.html' - path = Rack::Utils.clean_path_info(params[:path]) - path << ".#{Rack::Utils.clean_path_info(params[:format])}" unless path.ends_with?(params[:format].to_s) + path = Rack::Utils.clean_path_info(params[:path]).delete_prefix('/') + path << ".#{Rack::Utils.clean_path_info(params[:format]).delete_prefix('/')}" unless path.ends_with?(params[:format].to_s) path end end diff --git a/spec/controllers/swagger_controller_spec.rb b/spec/controllers/swagger_controller_spec.rb index f68bd6e9e..85afab318 100644 --- a/spec/controllers/swagger_controller_spec.rb +++ b/spec/controllers/swagger_controller_spec.rb @@ -8,5 +8,10 @@ describe '/swagger', type: :request do expect(response.body).to include('redoc') expect(response.body).to include('/swagger.json') end + + it 'does not render files outside the swagger directory' do + get '/swagger/%2Fetc%2Fpasswd' + expect(response).to have_http_status(:not_found) + end end end