Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
09fa2ae
Upgrade uri gem from 0.13.3 to 1.1.1 and fix all incompatible call sites
madnialihussain Jul 24, 2026
361e3b9
Eliminate all RFC2396 references and migrate to RFC3986 for uri gem 1…
madnialihussain Jul 30, 2026
46b5aff
Fix qlty/rubocop issues: PerlBackrefs, ConstantRegexp, EmptyLinesArou…
madnialihussain Jul 30, 2026
47252e4
Fix validation errors after replacing make_regexp with uri: validator
madnialihussain Jul 31, 2026
4cb28f4
fix webhook scenario to test validation error for invalid URL
madnialihussain Aug 12, 2026
8641c06
remove underscore from HOST regex
madnialihussain Aug 12, 2026
4d98455
revert sub chain consolidation in messages_helper
madnialihussain Aug 12, 2026
9f32039
remove double-escaping test from messages_helper_test
madnialihussain Aug 12, 2026
b216d16
add test for + in path for redhat auth callback
madnialihussain Aug 12, 2026
5b5bc85
add service_discovery auth callback referrer URL tests
madnialihussain Aug 12, 2026
794a67c
remove dead i18n :invalid entries for uri-validated fields
madnialihussain Aug 12, 2026
493a7cf
revert inlined patterns to URI::RFC2396 references
madnialihussain Aug 14, 2026
3dd8111
fix rubocop RedundantRegexpEscape for colon in messages_helper
madnialihussain Aug 14, 2026
4b7f655
replace URI.regexp with balanced-bracket regex in linkifier
madnialihussain Aug 14, 2026
6400cf4
extract UriPatterns module for RFC2396 pattern constants
madnialihussain Aug 18, 2026
99b10e6
add whitespace validation for site URL on Custom and Github auth prov…
madnialihussain Aug 18, 2026
e47b127
fix misleading comment in UriPatterns module
madnialihussain Aug 19, 2026
f720960
Remove unnecessary allow_blank from whitespace validators
madnialihussain Aug 19, 2026
2ed9e4c
Add test for sub-delimiter characters in URI validation
madnialihussain Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,7 @@ gem 'formtastic', '~> 5.0'
gem 'htmlentities', '~>4.3', '>= 4.3.4'
gem 'json', '~> 2.7', '>= 2.7.1'
gem 'responders', '~> 3.2' # For respond_with support
# uri >= 1.0.0 switched the default parser from RFC2396 to RFC3986 (ruby/uri#107),
# removed URI::DEFAULT_PARSER, dropped URI.decode, and the RFC3986 parser has no
# `registry` component. We rely on all of these across models (Proxy, ProxyRule,
# WebHook, AuthenticationProvider) and controllers. Upgrading is worthwhile but
# requires careful inspection of every call-site and should be deployed/tested
# separately from the Faraday update.
gem 'uri', '< 1.0.0'
gem 'uri', '~> 1.0'

gem 'mysql2', '~> 0.5.3'

Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ GEM
rack
unicorn
uniform_notifier (1.18.0)
uri (0.13.3)
uri (1.1.1)
useragent (0.16.11)
version_gem (1.1.3)
webmock (3.24.0)
Expand Down Expand Up @@ -1170,7 +1170,7 @@ DEPENDENCIES
uglifier
unicorn
unicorn-rails
uri (< 1.0.0)
uri (~> 1.0)
webmock (~> 3.24.0)
webrick (~> 1.8.2)
will_paginate (~> 3.3)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/provider/admin/redhat/auth_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def authentication_provider
def referrer_url
url = params.permit(:referrer)[:referrer]
if url
URI.decode(url)
CGI.unescapeURIComponent(url)
else
provider_admin_account_path
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def show
protected

def referrer_url
url = params[:referrer]
url = params.permit(:referrer)[:referrer]
Comment thread
jlledom marked this conversation as resolved.
if url
URI.decode(url)
CGI.unescapeURIComponent(url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: CGI.unescapeURIComponent raises ArgumentError on malformed percent-encoding (e.g. %ZZ), whereas the old URI.decode silently passed them through. Since referrer URLs originate from the app this is very unlikely, but is it worth a rescue here to fall back to the raw URL on ArgumentError?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The referrer URL is generated by our app and passed through the OAuth redirect flow. Malformed percent encoding would only occur if the user tampered with their own callback URL, which isn't a scenario we need to guard against

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: CGI.unescapeURIComponent raises ArgumentError on malformed percent-encoding (e.g. %ZZ), whereas the old URI.decode silently passed them through. Since referrer URLs originate from the app this is very unlikely, but is it worth a rescue here to fall back to the raw URL on ArgumentError?

Comment thread
jlledom marked this conversation as resolved.
else
new_admin_service_path
end
Expand Down
5 changes: 2 additions & 3 deletions app/helpers/messages_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ def message_subject(message)
def hyperlink_urls(text)
text = h(text)

text.scan(URI.regexp(%w(http https))) do
#$& contains the whole match of the regural expression
url = $&.sub(/\.$/, '').sub(/\:$/,'')
text.scan(%r{https?://[^\s)\]>]+}) do
Comment thread
jlledom marked this conversation as resolved.
Outdated
url = $&.sub(/[.:]+\z/, '')
Comment thread
qltysh[bot] marked this conversation as resolved.
Outdated
text = text.sub(url, link_to(url, url))
end

Expand Down
2 changes: 1 addition & 1 deletion app/lib/three_scale/oauth2/service_discovery_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def query_options
def call
url = super
# OpenShift builtin OAuth has a serious bug, it does not store correctly the redirect_uri
ServiceDiscovery::Config.rh_sso? ? url : URI.decode(url)
ServiceDiscovery::Config.rh_sso? ? url : CGI.unescapeURIComponent(url)
rescue => e
# Do better error management
Rails.logger.debug("[Openshift OAuth] Error decoding callback URL for builtin <%s>. Error: %s\n%s" % [url.inspect, e.message, e.backtrace.join("\n")])
Expand Down
2 changes: 1 addition & 1 deletion app/models/authentication_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class AuthenticationProvider < ApplicationRecord

validates :client_id, :client_secret, presence: true, if: :oauth_config_required?

with_options format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true, message: :invalid_url } do |ops|
with_options uri: { path: true, query: true, fragment: true }, allow_blank: true do |ops|
ops.validates :site
ops.validates :token_url
ops.validates :authorize_url
Expand Down
13 changes: 8 additions & 5 deletions app/models/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ class Proxy < ApplicationRecord # rubocop:disable Metrics/ClassLength

validates :error_status_no_match, :error_status_auth_missing, :error_status_auth_failed, :error_status_limits_exceeded, presence: true

uri_pattern = URI::DEFAULT_PARSER.pattern
pchar = "(?:[\\-_.!~*'()a-zA-Z\\d:@&=+$,]|%[a-fA-F\\d]{2})"
segment = "#{pchar}*(?:;#{pchar}*)*"
abs_path = "/#{segment}(?:/#{segment})*"
query = "(?:[\\-_.!~*'()a-zA-Z\\d;/?:@&=+$,\\[\\]]|%[a-fA-F\\d]{2})*"
optional_query = "(?:\\?(#{query}))?"
URI_PATH_PART = Regexp.new('\A' + abs_path + optional_query + '\z')
HOST = /\A(?:[a-zA-Z0-9\-._]|%\h\h)+(?::\d+)?\z/

@akostadinov akostadinov Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain how did you come up with these and how do we know these are correct? If this is a copy of what we previously used, that's also fine but we need to be sure that what we don't us some AI generated potentially buggy stuff.

I see you said you verified it's the same as in old parser.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, its copy of original, and i verified them all in rails console, only difference is HOST, just added _ a for RFC3986 compliance

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I don't agree that we should start allowing underscores in hostnames. I think we should keep rejecting these. Is there a reason to allow them. If there is, I'm open to change my mind. Here and in the other places.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About this, can't we use the constants from URI::RFC3986_PARSER.regexp?

I don't think it makes sense to eliminate all references to RFC2396 but then hardcode a copy of its patterns. In fact, we are actually using RFC2396 anyway, just in a more complicated way.

If these values are exactly the same they were in RFC2396, then I think it's better to just mention RFC2396 and take them from the gem. If they are now different and meet RFC3986, then better create a helper module with all this regexps and use it from here or the validator.


URI_OR_LOCALHOST = /\A(https?:\/\/([a-zA-Z0-9._:\/?-])+|.*localhost.*)\Z/
OPTIONAL_QUERY_FORMAT = "(?:\\?(#{uri_pattern.fetch(:QUERY)}))?"
URI_PATH_PART = Regexp.new('\A' + uri_pattern.fetch(:ABS_PATH) + OPTIONAL_QUERY_FORMAT + '\z')
HOST = Regexp.new('\A' + uri_pattern.fetch(:HOSTNAME) + '(:\d+)?' + '\z')

OAUTH_PARAMS = /(\?|&)(scope=|state=|tok=)/

Expand Down Expand Up @@ -675,7 +678,7 @@ def call(attribute)

begin
uri = URI.parse(attribute_value)
value = URI::Generic.new(uri.scheme, uri.userinfo, uri.host, uri.port, uri.registry, uri.path, uri.opaque, uri.query, uri.fragment).to_s
value = URI::Generic.new(uri.scheme, uri.userinfo, uri.host, uri.port, nil, uri.path, uri.opaque, uri.query, uri.fragment).to_s
Comment thread
qltysh[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 8 issues:

1. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]


2. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]


3. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]


4. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]


5. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]


6. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]


7. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]


8. Proxy::PortGenerator#call refers to 'uri' more than self (maybe move it to another class?) [reek:FeatureEnvy]

@model[attribute] = value unless @model[attribute] == value
rescue URI::InvalidURIError
@model.errors.add(attribute, 'Invalid domain')
Expand Down
17 changes: 10 additions & 7 deletions app/models/proxy_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ class PatternParser
REGEX_LITERAL = /[_\w]+/i
REGEX_VARIABLE = /\{#{REGEX_LITERAL}\}/

# pchar = unreserved | escaped |
# ":" | "@" | "&" | "=" | "+" | "$" | ","
UNRESERVED = "\\-_.!~*'()a-zA-Z\\d"
ESCAPED = "%[a-fA-F\\d]{2}"
RESERVED = ";/?:@&=+$,\\[\\]"

@akostadinov akostadinov Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here
I assume you verified these as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p URI::RFC2396_REGEXP::PATTERN::UNRESERVED etc

Comment thread
jlledom marked this conversation as resolved.
Outdated

# pchar = unreserved / pct-encoded / ":" / "@" / "&" / "=" / "+" / "," ($ excluded intentionally)
param = /
(?:
[#{URI::REGEXP::PATTERN::UNRESERVED}:@&=+,] # note that $ is in the RFC but is removed for our purpposes
[#{UNRESERVED}:@&=+,] # note that $ is in the RFC but is removed for our purpposes
|
#{URI::REGEXP::PATTERN::ESCAPED}
#{ESCAPED}
|
#{REGEX_VARIABLE}
)*
Comment thread
qltysh[bot] marked this conversation as resolved.
Expand All @@ -53,9 +56,9 @@ class PatternParser

query = /
(?:
[#{URI::REGEXP::PATTERN::UNRESERVED}#{URI::REGEXP::PATTERN::RESERVED}]
[#{UNRESERVED}#{RESERVED}]
|
#{URI::REGEXP::PATTERN::ESCAPED}
#{ESCAPED}
|
#{REGEX_LITERAL}=#{REGEX_VARIABLE}
)*
Comment thread
qltysh[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -89,7 +92,7 @@ def call(_)
validates :http_method, inclusion: { in: ALLOWED_HTTP_METHODS }
validate :non_repeated_parameters
validate :no_vars_in_keys
validates :redirect_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true, length: { maximum: 10000 }
validates :redirect_url, uri: { path: true, query: true, fragment: true }, allow_blank: true, length: { maximum: 10000 }

def parameters
Addressable::Template.new(path_pattern).variables
Expand Down
3 changes: 2 additions & 1 deletion app/models/web_hook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ class WebHook < ApplicationRecord
alias provider account

validates :account_id, presence: true
validates :url, format: { :with => URI::DEFAULT_PARSER.make_regexp(%w[http https]), :if => :active }, length: { maximum: 255 }
validates :url, uri: { path: true, query: true, fragment: true }, if: :active
validates :url, length: { maximum: 255 }

#TODO: limit association only to providers?
#TODO validate url as url?
Expand Down
6 changes: 2 additions & 4 deletions app/validators/uri_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ class UriValidator < ActiveModel::EachValidator
DEFAULT_PERMISSIONS_OF_PARTS = {
port: true,
userinfo: false,
registry: false,
path: false,
opaque: false,
query: false,
fragment: false
}.freeze
Expand Down Expand Up @@ -50,11 +48,11 @@ def initialize(uri:, permissions_parts:, accepted_scheme:)
@accepted_scheme = accepted_scheme
end

attr_reader :uri, :permissions_parts, :accepted_scheme, :generic_error_message
attr_reader :uri, :permissions_parts, :accepted_scheme
Comment thread
qltysh[bot] marked this conversation as resolved.
delegate :host, :scheme, to: :uri

def errors
return [generic_error_message] if uri.blank?
return [:invalid] if uri.blank?

errors_scheme | errors_host | errors_forbidden_parts
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ def setup
assert_equal ["can't be blank"], JSON.parse(response.body).dig('errors', 'site')

post admin_api_account_authentication_providers_path(authentication_provider_params(different_attributes: {kind: 'keycloak', site: 'invalid'}))
assert_equal ['Invalid URL format'], JSON.parse(response.body).dig('errors', 'site')
assert_equal ['invalid'], JSON.parse(response.body).dig('errors', 'site')

post admin_api_account_authentication_providers_path(authentication_provider_params(different_attributes: {kind: 'keycloak', site: ' http://example.com '}))
assert_equal ["can't contain whitespaces"], JSON.parse(response.body).dig('errors', 'site')
assert_equal ["invalid", "can't contain whitespaces"], JSON.parse(response.body).dig('errors', 'site')
end

test '#create ensures provider can use provider_sso' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ def setup
assert_equal ["can't be blank"], JSON.parse(response.body).dig('errors', 'site')

post admin_api_authentication_providers_path(authentication_provider_params(different_attributes: {kind: 'keycloak', site: 'invalid'}))
assert_equal ['Invalid URL format'], JSON.parse(response.body).dig('errors', 'site')
assert_equal ['invalid'], JSON.parse(response.body).dig('errors', 'site')

post admin_api_authentication_providers_path(authentication_provider_params(different_attributes: {kind: 'keycloak', site: ' http://example.com '}))
assert_equal ["can't contain whitespaces"], JSON.parse(response.body).dig('errors', 'site')
assert_equal ["invalid", "can't contain whitespaces"], JSON.parse(response.body).dig('errors', 'site')
end

test '#update saves the new attributes values' do
Expand Down
13 changes: 13 additions & 0 deletions test/integration/provider/admin/redhat/auth_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ def setup
end
end

test 'callback decodes referrer url' do
login_provider @provider
host! @provider.external_admin_domain
user_data = ThreeScale::OAuth2::UserData.new(username: 'redhat_user')
ThreeScale::OAuth2::KeycloakClient.any_instance.stubs(:authenticate!).returns(user_data)

get @callback_url, params: { referrer: '/p/admin/dashboard%3Ffoo%3Dbar' }
assert_redirected_to '/p/admin/dashboard?foo=bar'

get @callback_url, params: { referrer: '/p/admin/search?q=hello+world' }
assert_redirected_to '/p/admin/search?q=hello+world'
Comment thread
jlledom marked this conversation as resolved.
end

test 'Red Hat Customer Portal disabled' do
ThreeScale.config.redhat_customer_portal.stubs(enabled: false)

Expand Down
2 changes: 1 addition & 1 deletion test/unit/authentication_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class AuthenticationProviderTest < ActiveSupport::TestCase
errors = authentication_provider.errors

assert_equal %i[authorize_url site token_url user_info_url], errors.map(&:attribute).sort
assert_equal ['Invalid URL format'], errors.map(&:message).uniq
assert_equal ['invalid'], errors.map(&:message).uniq

authentication_provider.site = 'https://example.org'
authentication_provider.token_url = 'http://example.org'
Expand Down
5 changes: 5 additions & 0 deletions test/unit/helpers/messages_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ def test_if_links_with_colon_at_the_end_are_shown_properly_with_http_links
hyperlink_urls('Some http://google.com: text')
end

def test_if_links_with_query_string_are_fully_captured
assert_equal 'Visit <a href="http://google.com/path?q=1&amp;amp;r=2">http://google.com/path?q=1&amp;amp;r=2</a> now',
Comment thread
jlledom marked this conversation as resolved.
Outdated
hyperlink_urls('Visit http://google.com/path?q=1&r=2 now')
end

# this is regression test for: https://github.com/3scale/system/issues/4819
def test_if_random_word_with_colon_at_the_end_is_not_treated_like_link
assert_equal %(Some random word:),
Expand Down
5 changes: 4 additions & 1 deletion test/unit/proxy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ def test_deployable

@proxy.hostname_rewrite = 'my-own.api.example.net::80'
refute @proxy.valid?

@proxy.hostname_rewrite = 'my_proxy.internal'
assert @proxy.valid?
end

test 'backend' do
Expand Down Expand Up @@ -342,7 +345,7 @@ def test_deployable
end

test 'api_test_path formats valid' do
[ '/', '/i/m/a/lumberjack/42', '/~stuff', '/!not_-here']. each do |path|
[ '/', '/i/m/a/lumberjack/42', '/~stuff', '/!not_-here', '/path?key=value&other=123']. each do |path|
Comment thread
qltysh[bot] marked this conversation as resolved.
Outdated
@proxy.api_test_path = path
@proxy.valid?
assert_empty @proxy.errors[:api_test_path], "errors found on - #{path}"
Expand Down
26 changes: 26 additions & 0 deletions test/unit/web_hook_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ class PushingBehaviourTest < ActiveSupport::TestCase
end
end

test 'validate url' do
hook = FactoryBot.build_stubbed(:web_hook, active: true)

hook.url = 'http://example.com'
assert_valid hook

hook.url = 'https://example.com/webhook?token=abc'
assert_valid hook

hook.url = 'foo'
refute_valid hook
assert hook.errors[:url].present?

hook.url = 'ftp://example.com'
refute_valid hook
assert hook.errors[:url].present?

hook.url = ''
refute_valid hook
assert hook.errors[:url].present?

hook.active = false
hook.url = 'not-a-url'
assert_valid hook
end

test '#ping' do
hook = WebHook.new(url: "http://foo")

Expand Down