diff --git a/app/actions/route_create.rb b/app/actions/route_create.rb index 5a494b798e1..e5ccee5f59a 100644 --- a/app/actions/route_create.rb +++ b/app/actions/route_create.rb @@ -93,9 +93,12 @@ def validation_error_quota!(error, space) end def validation_error_route!(error) - return unless error.errors.on(:route)&.include?(:hash_header_missing) + error!('Hash header must be present when loadbalancing is set to hash.') if error.errors.on(:route)&.include?(:hash_header_missing) - error!('Hash header must be present when loadbalancing is set to hash.') + return unless error.errors.on(:route)&.include?(:options_size_exceeded) + + max_size = Config.config.get(:max_route_options_size) + error!("Route options size exceeded: options must be smaller than #{max_size} bytes.") end def validation_error_routing_api!(error) diff --git a/app/actions/route_update.rb b/app/actions/route_update.rb index df3a325f378..a4bd9d7321c 100644 --- a/app/actions/route_update.rb +++ b/app/actions/route_update.rb @@ -26,6 +26,12 @@ def validation_error!(error) # Handle hash_header validation error for hash loadbalancing raise Error.new('Hash header must be present when loadbalancing is set to hash.') if error.errors.on(:route)&.include?(:hash_header_missing) + # Handle route options size exceeded error + if error.errors.on(:route)&.include?(:options_size_exceeded) + max_size = Config.config.get(:max_route_options_size) + raise Error.new("Route options size exceeded: options must be smaller than #{max_size} bytes.") + end + # Fallback for any other validation errors raise Error.new(error.message) end diff --git a/app/messages/validators.rb b/app/messages/validators.rb index 48d4d3bdf61..6d660a6d5db 100644 --- a/app/messages/validators.rb +++ b/app/messages/validators.rb @@ -247,6 +247,8 @@ def validate(record) return end + validate_size(record) + opt = record.options_message return if opt.valid? @@ -255,6 +257,17 @@ def validate(record) record.errors.add(:options, message:) end end + + private + + def validate_size(record) + max_size = VCAP::CloudController::Config.config.get(:max_route_options_size) + options_size = record.options.to_json.bytesize + + return unless options_size > max_size + + record.errors.add(:options, message: "must be smaller than #{max_size} bytes (actual size is #{options_size} bytes)") + end end class ToOneRelationshipValidator < ActiveModel::EachValidator diff --git a/app/models/runtime/route.rb b/app/models/runtime/route.rb index bdefff78c41..e5bf9844c49 100644 --- a/app/models/runtime/route.rb +++ b/app/models/runtime/route.rb @@ -326,6 +326,20 @@ def validate_total_reserved_route_ports def validate_route_options return if options.blank? + validate_route_options_size + validate_route_options_hash_header + end + + def validate_route_options_size + max_size = Config.config.get(:max_route_options_size) + options_size = options.to_json.bytesize + + return unless options_size > max_size + + errors.add(:route, :options_size_exceeded) + end + + def validate_route_options_hash_header route_options = options.is_a?(Hash) ? options : options.symbolize_keys loadbalancing = route_options[:loadbalancing] || route_options['loadbalancing'] diff --git a/config/cloud_controller.yml b/config/cloud_controller.yml index bdb1f9108f5..5cf9aad4241 100644 --- a/config/cloud_controller.yml +++ b/config/cloud_controller.yml @@ -161,6 +161,7 @@ routing_api: route_services_enabled: true volume_services_enabled: true disable_private_domain_cross_space_context_path_route_sharing: false +max_route_options_size: 1024 quota_definitions: default: diff --git a/lib/cloud_controller/config_schemas/api_schema.rb b/lib/cloud_controller/config_schemas/api_schema.rb index 26cc02896eb..f7c0257e953 100644 --- a/lib/cloud_controller/config_schemas/api_schema.rb +++ b/lib/cloud_controller/config_schemas/api_schema.rb @@ -35,6 +35,7 @@ class ApiSchema < VCAP::Config optional(:system_domain_organization) => enum(String, NilClass), app_domains: Array, disable_private_domain_cross_space_context_path_route_sharing: bool, + max_route_options_size: Integer, cpu_weight_min_memory: Integer, cpu_weight_max_memory: Integer, diff --git a/lib/cloud_controller/config_schemas/worker_schema.rb b/lib/cloud_controller/config_schemas/worker_schema.rb index 054fccf4c30..9eb8b3bc557 100644 --- a/lib/cloud_controller/config_schemas/worker_schema.rb +++ b/lib/cloud_controller/config_schemas/worker_schema.rb @@ -14,6 +14,7 @@ class WorkerSchema < VCAP::Config external_protocol: String, internal_service_hostname: String, disable_private_domain_cross_space_context_path_route_sharing: bool, + max_route_options_size: Integer, readiness_port: { cloud_controller_worker: Integer }, diff --git a/spec/unit/actions/route_create_spec.rb b/spec/unit/actions/route_create_spec.rb index 86fdca59888..26780edb0f9 100644 --- a/spec/unit/actions/route_create_spec.rb +++ b/spec/unit/actions/route_create_spec.rb @@ -190,6 +190,37 @@ module VCAP::CloudController end.to raise_error(RouteCreate::Error, 'Hash header must be present when loadbalancing is set to hash.') end end + + context 'when options size exceeds the configured limit' do + before do + TestConfig.override(max_route_options_size: 50) + # Enable hash_based_routing feature to allow hash_header in options + VCAP::CloudController::FeatureFlag.make(name: 'hash_based_routing', enabled: true, error_message: nil) + end + + let(:message_with_large_options) do + RouteCreateMessage.new({ + relationships: { + space: { + data: { guid: space.guid } + }, + domain: { + data: { guid: domain.guid } + } + }, + options: { + loadbalancing: 'hash', + hash_header: 'X-Custom-Header-Name' + } + }) + end + + it 'raises an error indicating options size exceeded' do + expect do + subject.create(message: message_with_large_options, space: space, domain: domain) + end.to raise_error(RouteCreate::Error, /Route options size exceeded: options must be smaller than 50 bytes/) + end + end end context 'when creating a route with other loadbalancing options' do diff --git a/spec/unit/actions/route_update_spec.rb b/spec/unit/actions/route_update_spec.rb index be966d96fe2..f3cd51ae526 100644 --- a/spec/unit/actions/route_update_spec.rb +++ b/spec/unit/actions/route_update_spec.rb @@ -511,6 +511,30 @@ module VCAP::CloudController end end end + + context 'when options size exceeds the configured limit' do + before do + TestConfig.override(max_route_options_size: 50) + # Enable hash_based_routing feature to allow hash_header in options + VCAP::CloudController::FeatureFlag.make(name: 'hash_based_routing', enabled: true, error_message: nil) + route[:options] = '{"loadbalancing": "round-robin"}' + end + + let(:body) do + { + options: { + loadbalancing: 'hash', + hash_header: 'a' * 100 + } + } + end + + it 'raises an error indicating options size exceeded' do + expect do + subject.update(route:, message:) + end.to raise_error(RouteUpdate::Error, /Route options size exceeded: options must be smaller than 50 bytes/) + end + end end end end diff --git a/spec/unit/messages/route_create_message_spec.rb b/spec/unit/messages/route_create_message_spec.rb index c77b9c6d15e..b209f57b1dd 100644 --- a/spec/unit/messages/route_create_message_spec.rb +++ b/spec/unit/messages/route_create_message_spec.rb @@ -629,6 +629,49 @@ module VCAP::CloudController end end end + + context 'when options size exceeds the configured limit' do + before do + TestConfig.override(max_route_options_size: 50) + end + + let(:params) do + { + host: 'some-host', + relationships: { + space: { data: { guid: 'space-guid' } }, + domain: { data: { guid: 'domain-guid' } } + }, + options: { loadbalancing: 'round-robin', some_large_key: 'a' * 100 } + } + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors[:options].to_s).to include('must be smaller than 50 bytes') + end + end + + context 'when options size is within the configured limit' do + before do + TestConfig.override(max_route_options_size: 1024) + end + + let(:params) do + { + host: 'some-host', + relationships: { + space: { data: { guid: 'space-guid' } }, + domain: { data: { guid: 'domain-guid' } } + }, + options: { loadbalancing: 'round-robin' } + } + end + + it 'is valid' do + expect(subject).to be_valid + end + end end end diff --git a/spec/unit/models/runtime/route_spec.rb b/spec/unit/models/runtime/route_spec.rb index c6554939adc..0812d38bef3 100644 --- a/spec/unit/models/runtime/route_spec.rb +++ b/spec/unit/models/runtime/route_spec.rb @@ -1476,6 +1476,43 @@ module VCAP::CloudController end end + context 'when options size exceeds the configured limit' do + before do + TestConfig.override(max_route_options_size: 50) + end + + it 'is invalid and adds an error' do + large_options = { loadbalancing: 'round-robin', some_large_key: 'a' * 100 } + route = Route.new( + host: 'test-route', + domain: domain, + space: space, + options: large_options + ) + + expect(route).not_to be_valid + expect(route.errors[:route]).to include :options_size_exceeded + end + end + + context 'when options size is within the configured limit' do + before do + TestConfig.override(max_route_options_size: 1024) + end + + it 'is valid' do + small_options = { loadbalancing: 'round-robin' } + route = Route.new( + host: 'test-route', + domain: domain, + space: space, + options: small_options + ) + + expect(route).to be_valid + end + end + context 'when updating an existing route' do context 'changing to hash loadbalancing without hash_header' do it 'is invalid' do