diff --git a/config/application.rb b/config/application.rb index 73b8deeb233..fb61d718736 100644 --- a/config/application.rb +++ b/config/application.rb @@ -197,6 +197,9 @@ module Discourse # supports etags (post 1.7) config.middleware.delete Rack::ETag + require 'middleware/enforce_hostname' + config.middleware.insert_after Rack::MethodOverride, Middleware::EnforceHostname + require 'content_security_policy' config.middleware.swap ActionDispatch::ContentSecurityPolicy::Middleware, ContentSecurityPolicy::Middleware diff --git a/lib/middleware/enforce_hostname.rb b/lib/middleware/enforce_hostname.rb new file mode 100644 index 00000000000..130cabb87a9 --- /dev/null +++ b/lib/middleware/enforce_hostname.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Middleware + class EnforceHostname + def initialize(app, settings = nil) + @app = app + end + + def call(env) + # enforces hostname to match the hostname of our connection + # this middleware lives after rails multisite so at this point + # Discourse.current_hostname MUST be canonical, enforce it so + # all Rails helpers are guarenteed to use it unconditionally and + # never generate incorrect links + env[Rack::Request::HTTP_X_FORWARDED_HOST] = nil + env[Rack::HTTP_HOST] = Discourse.current_hostname + @app.call(env) + end + end +end diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index a2a63fb1c57..30d2a45e8ba 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -213,6 +213,19 @@ RSpec.describe ApplicationController do end end + describe 'Custom hostname' do + + it 'does not allow arbitrary host injection' do + get("/latest", + headers: { + "X-Forwarded-Host" => "test123.com" + } + ) + + expect(response.body).not_to include("test123") + end + end + describe 'Content Security Policy' do it 'is enabled by SiteSettings' do SiteSetting.content_security_policy = false