Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handle basic authentication without a password #44610

Merged
merged 1 commit into from Mar 4, 2022

Conversation

casperisfine
Copy link
Contributor

#43209 immediately rejects the request if no password is passed, but there are legitimate uses for accepting authentication without a password.

cc @mpestov, @rafaelfranca, @p8

@rails-bot rails-bot bot added the actionpack label Mar 4, 2022
@p8
Copy link
Member

p8 commented Mar 4, 2022

Makes sense to me!

authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
# This comparison uses & so that it doesn't short circuit and
# uses `secure_compare` so that length information isn't leaked.
ActiveSupport::SecurityUtils.secure_compare(given_name, name) &
ActiveSupport::SecurityUtils.secure_compare(given_password, password)
ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) &
Copy link
Member

@p8 p8 Mar 4, 2022

Choose a reason for hiding this comment

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

Is there a reason name.to_s isn't called here instead?
Just wondering :)

Suggested change
ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) &
ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name.to_s) &

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a bit handwavy, but the idea was to cast to string once and for all at boot, so that if somehow you passed something that acts as a string but isn't, we don't open a timing attack (because presumably to_s would take longer, the longer the password is).

But again it wasn't to prevent a specific scenario, just to be extra careful. Anyway I refactored the PR, so this to_s is no more.

@@ -79,11 +79,13 @@ def http_basic_authenticate_with(name:, password:, realm: nil, **options)
end

def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
name = name.to_s
password = password.to_s
Copy link
Member

Choose a reason for hiding this comment

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

How about nil -> raise because that might mean you've failed to pull a value out of ENV etc, "" -> acceptable slightly-more-explicit statement that you intend to allow a blank password?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I agrees this isn't ideal, I actually don't want to allow nil when http_basic_authenticate_or_request_with is used, only authenticate_with_http_basic do.

@mpestov
Copy link

mpestov commented Mar 4, 2022

@casperisfine I think it is not legitimate to use authentication without a password.
We implement HTTP Basic Authentication as described in https://datatracker.ietf.org/doc/html/rfc2617.

The server will service the request only if it can validate
the user-ID and password for the protection space of the Request-URI.
There are no optional authentication parameters.

@casperisfine
Copy link
Contributor Author

@mpestov that doesn't mean a missing password can't be valid. I've used https://<api-token>@example.com for years: https://github.com/Shopify/shipit-engine/blob/84d01d09156981ec539497599c6692c0be681f44/app/controllers/shipit/api/base_controller.rb#L39-L42

rails#43209 immediately rejects
the request if no password is passed, but there are legitimate
uses for accepting authentication without a password.
@byroot byroot merged commit 9ae0521 into rails:main Mar 4, 2022
@@ -74,6 +74,8 @@ module ClassMethods
#
# See ActionController::HttpAuthentication::Basic for example usage.
def http_basic_authenticate_with(name:, password:, realm: nil, **options)
raise ArgumentError, "Expected name: to be a String, got #{name.class}" unless name.is_a?(String)
raise ArgumentError, "Expected password: to be a String, got #{password.class}" unless name.is_a?(String)
Copy link

Choose a reason for hiding this comment

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

I assume this is supposed to be unless password.is_a?(String)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤦 yes you are right. I'll fix directly in main. Thanks!

@mpestov
Copy link

mpestov commented Mar 4, 2022

@casperisfine In my opinion your case is the Http Token authentication not Basic.
I would use authenticate_with_http_token for this.

authenticate_with_http_token do |*parts|
  token = parts.select(&:present?).join('--')
  ApiClient.authenticate(token)
end

For example Rack uses the same validation: https://github.com/rack/rack/blob/main/lib/rack/auth/basic.rb#L47

@lonesomewalker
Copy link

@mpestov I can assure you, there is A LOT of legitimate use.
Especially when you use ip based authentication.

@mpestov
Copy link

mpestov commented Apr 12, 2022

@lonesomewalker Yeah! You are right.
I've researched recently about it, many frameworks supported using the access token as username in the basic auth and password is not mandatory in this case. 
Unfortunately, it is not obvious a use case for me, so I did not support this in my PR.  

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants