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

Fix sensitive data leaking in Authentication #1067

Merged
merged 2 commits into from
Jul 29, 2024

Conversation

jagerman
Copy link
Contributor

The current Authentication constructor has multiple points where a copy can get made: in the arguments themselves, in the intermediate concatenations, and in the potential need for the concatenation to copy itself during a memory reallocation.

An additional copy of the auth data could end up unwiped in the implicit move constructor/assignment (in particular when small string optimization applies to the value).

Any such copies end up potentially leaving the sensitive data behind in memory, undermining the changes in #776 that were trying to securely erase such sensitive data.

This commit avoids any such copies by:

  • changing Authentication to take std::string_views (instead of std::string) for username and password so that no copy of input will be done
  • properly reserving auth_string_ to its required size before building it
  • Adding an explicit move constructor that resizes the moved-from auth string to capacity to ensure it gets erased when SSO applies.
  • Adding an explicit move assignment operator that wipes the current value before replacing it, and properly resizes the moved-from string to capacity to ensure it gets wiped when SSO applies.

@jagerman jagerman force-pushed the secure-auth-construction branch 2 times, most recently from b4ec38c to 0b11488 Compare June 21, 2024 21:43
The current Authentication constructor has multiple points where a copy
can get made: in the arguments themselves, in the intermediate
concatenations, and in the potential need for the concatenation to copy
itself during a memory reallocation.

An additional copy of the auth data could end up unwiped in the implicit
move constructor/assignment (in particular when small string
optimization applies to the value).

Any such copies end up potentially leaving the sensitive data behind in
memory, undermining the changes in libcpr#776 that were trying to securely
erase such sensitive data.

This commit avoids any such copies by:
- changing Authentication to take string_views (instead of std::string)
  for username and password so that no copy of input will be done
- properly reserving auth_string_ to its required size before building
  it
- resizing the auth_string_ of moved-from values to their capacity so
  that secureStringClear will properly erase them.
- Adding an explicit move constructor that resizes the moved-from auth
  string to capacity to ensure it gets erased when SSO applies.
- Adding an explicit move assignment operator that wipes the current
  value before replacing it, and properly resizes the moved-from string
  to capacity to ensure it gets wiped when SSO applies.
Copy link
Member

@COM8 COM8 left a comment

Choose a reason for hiding this comment

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

Good idea, thanks @jagerman!

I found two use after move cases that you want to look at other than that LGTM.

cpr/auth.cpp Outdated
Comment on lines 16 to 18
Authentication::Authentication(Authentication&& old) noexcept : auth_string_{std::move(old.auth_string_)}, auth_mode_{old.auth_mode_} {
old.auth_string_.resize(old.auth_string_.capacity());
}
Copy link
Member

Choose a reason for hiding this comment

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

This is probably not what you want since after moving old.auth_string_ is not guaranteed thet old.auth_string_.capacity() returns any value that makes sense.

Better:

Suggested change
Authentication::Authentication(Authentication&& old) noexcept : auth_string_{std::move(old.auth_string_)}, auth_mode_{old.auth_mode_} {
old.auth_string_.resize(old.auth_string_.capacity());
}
Authentication::Authentication(Authentication&& old) noexcept auth_mode_{old.auth_mode_} {
auth_string_ = old.auth_string_;
util::secureStringClear(old.auth_string_);
}

Copy link
Contributor Author

@jagerman jagerman Jun 24, 2024

Choose a reason for hiding this comment

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

This is probably not what you want since after moving old.auth_string_ is not guaranteed thet old.auth_string_.capacity() returns any value that makes sense.

I think it still makes sense: the moved-from string is still required to have a valid state. It'll almost certainly return the capacity of an SSO string (since it either was an SSO string to begin with, or else transferred away its pointer and would have returned to being a 0-length SSO string).

In the case that it was SSO to start with, the old value will still be there, inside the SSO storage of the string itself. If it wasn't SSO to start with, this is indeed a useless operation -- but unfortunately there's no interface to ask std::string if it was applying SSO or not.

However, if you'd rather not invoke the string's move constructor like this then I think we should just delete the move constructor and assignment from Authentication entirely as, with a copy of the string like in the suggestion, it isn't actually doing anything that a copy constructor wouldn't do anyway. (The only effect would be a slight reordering of calling secureStringClear here instead of waiting for it to happen in the destruction of the copied-from object).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another option that might be preferable: change auth_string_ to a std::vector<char>, which does have a guarantee that the memory only exists in one place and is always transferred by a move.

That would actually simplify this quite a bit: we could use default move constructors that would do the right thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've pushed the std::vector version, currently as a separate commit for discussion, but I'll squash those if it looks good.

Copy link
Member

Choose a reason for hiding this comment

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

I see your point there and the std::vector approach somehow resolves it. But I really do not like this approach since it does not feel like C++ it feels like C .

On the String move. There are no guarantees std::move gives you when moving an object that any of it's components might still be intact, even if we consider different implementations of std::string. So a potential use after move is always a bad idea in my eyes.

Deleting the move ctr is an option but I see my solution for the problem as a good and secure version where we do not have to make any assumptions.

Thanks for taking the time! I really appreciate it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deleting the move ctr is an option but I see my solution for the problem as a good and secure version where we do not have to make any assumptions.

I don't really like this move constructor approach because it signals efficient move semantics but then doesn't actually provide them (because the implement always copies); and so better I think to just delete the move ctor/assignment if you want to keep this as a string.

cpr/auth.cpp Outdated
Comment on lines 23 to 24
auth_string_ = std::move(old.auth_string_);
old.auth_string_.resize(old.auth_string_.capacity());
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
auth_string_ = std::move(old.auth_string_);
old.auth_string_.resize(old.auth_string_.capacity());
auth_string_ = old.auth_string_;
util::secureStringClear(old.auth_string_);

Again use after moved.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, let us delete the move semantics ctr and switch back from the std::vector based solution.

move semantics with a std::string introduce difficulty with secure
erasing because we have no guarantee that the moved-from object doesn't
still have the the credentials (because of small string optimization).

This removes the move ctor and assignment so that we always get secure
erasing on destruction without any weird SSO cases to worry about.
@jagerman
Copy link
Contributor Author

Updated to work with std::strings and without any (problematic) std::string moves.

Copy link
Member

@COM8 COM8 left a comment

Choose a reason for hiding this comment

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

@jagerman Thanks!

@COM8 COM8 merged commit 57e263d into libcpr:master Jul 29, 2024
49 of 53 checks passed
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.

2 participants