From 17e1bfe06971e9f508bc82e5053fdde7162e82ca Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Fri, 3 Jan 2025 12:21:17 +0800 Subject: [PATCH] SECURITY: Preload data only when rendering application layout This commit drops the `before_action :preload_json` callback in `ApplicationController` as it adds unnecessary complexity to `ApplicationController` as well as other controllers which has to skip this callback. The source of the complexity comes mainly from the following two conditionals in the `preload_json` method: ``` # We don't preload JSON on xhr or JSON request return if request.xhr? || request.format.json? # if we are posting in makes no sense to preload return if request.method != "GET" ``` Basically, the conditionals solely exists for optimization purposes to ensure that we don't run the preloading code when the request is not a GET request and the response is not expected to be HTML. The key problem here is that the conditionals are trying to expect what the content type of the response will be and this has proven to be hard to get right. Instead, we can simplify this problem by running the preloading code in a more deterministic way which is to preload only when the `application` layout is being rendered and this is main change that this commit introduces. --- app/controllers/application_controller.rb | 167 +++--------------- .../finish_installation_controller.rb | 1 - app/controllers/site_controller.rb | 6 +- app/helpers/application_helper.rb | 8 +- app/models/topic.rb | 4 +- lib/application_layout_preloader.rb | 146 +++++++++++++++ lib/db_helper.rb | 2 +- spec/helpers/application_helper_spec.rb | 18 +- .../requests/admin/backups_controller_spec.rb | 6 +- spec/requests/application_controller_spec.rb | 35 ---- 10 files changed, 201 insertions(+), 192 deletions(-) create mode 100644 lib/application_layout_preloader.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 64810414534..84a6ec562bb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -43,6 +43,7 @@ class ApplicationController < ActionController::Base before_action :block_if_requires_login before_action :redirect_to_profile_if_required before_action :preload_json + before_action :initialize_application_layout_preloader before_action :check_xhr after_action :add_readonly_header after_action :perform_refresh_session @@ -277,6 +278,7 @@ class ApplicationController < ActionController::Base def rescue_discourse_actions(type, status_code, opts = nil) opts ||= {} + show_json_errors = (request.format && request.format.json?) || (request.xhr?) || ((params[:external_id] || "").ends_with? ".json") @@ -339,8 +341,9 @@ class ApplicationController < ActionController::Base rescue Discourse::InvalidAccess return render plain: message, status: status_code end + with_resolved_locale do - error_page_opts[:layout] = (opts[:include_ember] && @preloaded) ? set_layout : "no_ember" + error_page_opts[:layout] = opts[:include_ember] && @_preloaded ? set_layout : "no_ember" render html: build_not_found_page(error_page_opts) end end @@ -424,31 +427,6 @@ class ApplicationController < ActionController::Base I18n.with_locale(locale) { yield } end - def store_preloaded(key, json) - @preloaded ||= {} - # I dislike that there is a gsub as opposed to a gsub! - # but we can not be mucking with user input, I wonder if there is a way - # to inject this safety deeper in the library or even in AM serializer - @preloaded[key] = json.gsub(" "iam badcrawler" } expect(response.status).to eq(429) end - - context "with XHR requests" do - before { global_setting :anon_cache_store_threshold, 1 } - - def preloaded_data - response_html = Nokogiri::HTML5.fragment(response.body) - JSON.parse(response_html.css("#data-preloaded")[0]["data-preloaded"]) - end - - it "does not return the same preloaded data for XHR and non-XHR requests" do - # Request is stored in cache - get "/", headers: { "X-Requested-With" => "XMLHTTPrequest" } - expect(response.status).to eq(200) - expect(response.headers["X-Discourse-Cached"]).to eq("store") - expect(preloaded_data).not_to have_key("site") - - # Request is served from cache - get "/", headers: { "X-Requested-With" => "xmlhttprequest" } - expect(response.status).to eq(200) - expect(response.headers["X-Discourse-Cached"]).to eq("true") - expect(preloaded_data).not_to have_key("site") - - # Request is not served from cache because of different headers, but is stored - get "/" - expect(response.status).to eq(200) - expect(response.headers["X-Discourse-Cached"]).to eq("store") - expect(preloaded_data).to have_key("site") - - # Request is served from cache - get "/", headers: { "X-Requested-With" => "xmlhttprequest" } - expect(response.status).to eq(200) - expect(response.headers["X-Discourse-Cached"]).to eq("true") - expect(preloaded_data).not_to have_key("site") - end - end end end