Skip to content

Commit fd567af

Browse files
committed
SECURITY: Error responses missing Cache-Control header
By default, we want all responses to have the `Cache-Control` header set to `no-cache, no-store`. Individual controller actions can override the header when need be.
1 parent c96aeda commit fd567af

File tree

3 files changed

+39
-1
lines changed

3 files changed

+39
-1
lines changed

app/controllers/application_controller.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def handle_unverified_request
3030
end
3131
end
3232

33+
around_action :ensure_dont_cache_page
3334
before_action :check_readonly_mode
3435
before_action :handle_theme
3536
before_action :set_current_user_for_logs
@@ -47,7 +48,6 @@ def handle_unverified_request
4748
before_action :check_xhr
4849
after_action :add_readonly_header
4950
after_action :perform_refresh_session
50-
after_action :dont_cache_page
5151
after_action :conditionally_allow_site_embedding
5252
after_action :ensure_vary_header
5353
after_action :add_noindex_header,
@@ -157,6 +157,11 @@ class PluginDisabled < StandardError
157157
end
158158

159159
rescue_from ActionController::RoutingError, PluginDisabled do
160+
# This error is raised outside of the normal request response cycle and is called via the
161+
# `DiscoursePublicExceptions` middleware which creates a new instance of the ApplicationController.
162+
# As a result, controller actions hooks are not called and we need to explicitly call `dont_cache_page` here.
163+
dont_cache_page
164+
160165
rescue_discourse_actions(:not_found, 404)
161166
end
162167

@@ -1055,4 +1060,10 @@ def clean_xml
10551060
def service_params
10561061
{ params: params.to_unsafe_h, guardian: }
10571062
end
1063+
1064+
def ensure_dont_cache_page
1065+
yield
1066+
ensure
1067+
dont_cache_page
1068+
end
10581069
end

lib/middleware/discourse_public_exceptions.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ def call(env)
6565
end
6666

6767
if ApplicationController.rescue_with_handler(exception, object: fake_controller)
68+
# Calling `ActionDispatch::Response#to_a` here to ensure that cache control headers are set.
69+
response.to_a
6870
body = response.body
6971
body = [body] if String === body
7072
rack_response = [response.status, response.headers, body]

spec/requests/application_controller_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
# frozen_string_literal: true
22

33
RSpec.describe ApplicationController do
4+
fab!(:user)
5+
6+
context "for cache control headers" do
7+
it "sets the `no-cache, no-store` cache control response header when no error is raised" do
8+
get "/latest"
9+
10+
expect(response.status).to eq(200)
11+
expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
12+
end
13+
14+
it "sets the `no-cache, no-store` cache control response header when ActionController::RoutingError is raised" do
15+
get "/invalid-urlllllllllll"
16+
17+
expect(response.status).to eq(404)
18+
expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
19+
end
20+
21+
it "sets the `no-cache, no-store` cache control response header when Discourse::InvalidAccess is raised" do
22+
get "/latest.json", headers: { HTTP_API_KEY: "invalid-api-key" }
23+
24+
expect(response.status).to eq(403)
25+
expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
26+
end
27+
end
28+
429
describe "#redirect_to_login_if_required" do
530
let(:admin) { Fabricate(:admin) }
631

0 commit comments

Comments
 (0)