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

Avoid tracking logins from Devise::Strategies::Rememberable #3867

Merged
merged 6 commits into from
Sep 18, 2024

Conversation

y9v
Copy link
Member

@y9v y9v commented Aug 27, 2024

What does this PR do?
In Devise integration Devise::Strategies::Authenticatable#validate is patched to track login successes and failures.

This method is not only used by Devise::Strategies::DatabaseAuthenticatable, but also by Devise::Strategies::Rememberable that is used for signing in the user using a remember token cookie.

Motivation:
This should reduce the amount of false-positive Login Success events for Devise AppSec integration.

Additional Notes:
I added an alias method for validate to avoid calling super from Rememberable strategy, since the super method is the validate from prepended AuthenticatablePatch.

How to test the change?
I tested this in a simple rails application with devise.
To test that regular login tracking works it's enough to log in via the sign-in form and observe a users.login.success tag on a trace.

To test that logins from Devise::Strategies::Rememberable strategy are not tracked:

  • Log in via the sign-in form with checked "Remember Me" checkbox
  • Go to any page that requires user authentication and delete the session cookie from Developer Tools
  • Reload the page, the user will still be logged in, and the session cookie should reappear
  • Observe that the trace was not tagged with users.login.success

@y9v y9v self-assigned this Aug 27, 2024
@y9v y9v requested a review from a team as a code owner August 27, 2024 13:40
@github-actions github-actions bot added appsec Application Security monitoring product integrations Involves tracing integrations labels Aug 27, 2024
@codecov-commenter
Copy link

codecov-commenter commented Aug 27, 2024

Codecov Report

Attention: Patch coverage is 86.48649% with 5 lines in your changes missing coverage. Please review.

Please upload report for BASE (master@63423f9). Learn more about missing BASE report.

Files with missing lines Patch % Lines
lib/datadog/appsec/contrib/devise/patcher.rb 37.50% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3867   +/-   ##
=========================================
  Coverage          ?   97.77%           
=========================================
  Files             ?     1286           
  Lines             ?    77059           
  Branches          ?     3807           
=========================================
  Hits              ?    75343           
  Misses            ?     1716           
  Partials          ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pr-commenter
Copy link

pr-commenter bot commented Aug 27, 2024

Benchmarks

Benchmark execution time: 2024-09-18 13:26:08

Comparing candidate commit fa16b64 in PR branch appsec-avoid-tracking-devise-rememberable-logins with baseline commit 63423f9 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 23 metrics, 2 unstable metrics.

@ivoanjo
Copy link
Member

ivoanjo commented Aug 27, 2024

Feel free to ignore the failing memory leaks test, fix is in #3868

@y9v y9v force-pushed the appsec-avoid-tracking-devise-rememberable-logins branch from 747d3ba to d3905a4 Compare August 27, 2024 17:03
patch_registration_controller

Patcher.instance_variable_set(:@patched, true)
end

def patch_authenticable_strategy
def patch_authenticatable_strategy
::Devise::Strategies::Authenticatable.alias_method(:__validate, :validate)
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest using a prefix/postfix that indicates the purpose and owner of this alias, e.g. something along the lines of __validate_datadog or __validate_datadog_authenticatable. With just a double-underscore prefix there is potential for collisions with other code and it's not clear what the purpose of the method is to someone who might be looking at it in their application for whatever reason (and has no knowledge of dd-trace-rb internals).

Copy link
Member Author

@y9v y9v Aug 28, 2024

Choose a reason for hiding this comment

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

Thank you, I renamed the alias in f2fe86a

@y9v y9v force-pushed the appsec-avoid-tracking-devise-rememberable-logins branch from 8ab07d8 to 5850767 Compare August 28, 2024 18:29
@y9v y9v requested a review from lloeki August 29, 2024 12:03
return unless ::Devise::STRATEGIES.include?(:rememberable)

# Rememberable strategy is required in autoloaded Rememberable model
::Devise::Models::Rememberable # rubocop:disable Lint/Void
Copy link
Contributor

@lloeki lloeki Sep 4, 2024

Choose a reason for hiding this comment

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

Not fixable, but oh my, I hate autoloading so much :(

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, especially when autoloaded classes require other classes/modules

patch_registration_controller

Patcher.instance_variable_set(:@patched, true)
end

def patch_authenticable_strategy
def patch_authenticatable_strategy
::Devise::Strategies::Authenticatable.alias_method(:__validate_datadog_authenticatable, :validate)
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks dangerous, as mixing aliasing and prepending can easily lead to obscure issues such as recursive loops.

AIUI the reason this was introduced is that the call stack is:

Foo#validate    # some app class that included ::Devise::Strategies::Rememberable and may implement validate that calls `super` or simply rely on it being provided by a parent
::Devise::Strategies::Rememberable#validate                                   # hypothetical, not there upstream as of yet
::Datadog::AppSec::Contrib::Devise::Patcher::AuthenticatablePatch#validate
::Devise::Strategies::Authenticatable#validate

But just prepending a patch results in this:

Foo#validate    # some app class that included ::Devise::Strategies::Rememberable and may implement validate that calls `super` or simply rely on it being provided by a parent
::Datadog::AppSec::Contrib::Devise::Patcher::RememberablePatch#validate
::Devise::Strategies::Rememberable#validate                                   # hypothetical, not there upstream as of yet
::Datadog::AppSec::Contrib::Devise::Patcher::AuthenticatablePatch#validate
::Devise::Strategies::Authenticatable#validate

But we want to skip some/all of ::Datadog::AppSec::Contrib::Devise::Patcher::AuthenticatablePatch#validate, so this attempts to walk around it with:

Foo#validate    # some app class that included ::Devise::Strategies::Rememberable and may implement validate that calls `super` or simply rely on it being provided by a parent
::Datadog::AppSec::Contrib::Devise::Patcher::RememberablePatch#validate
::Devise::Strategies::Authenticatable#__validate_datadog_authenticatable

The problems are:

  • suddenly ::Devise::Strategies::Authenticatable#validate is not called at all (the method body is, but not the name). This can cause issues: a third party instrumenting ::Devise::Strategies::Authenticatable#validate as well would never be called, or if ::Devise::Strategies::Authenticatable#validate's source code calls super it will actually look for __validate_datadog_authenticatable in a parent.
  • Between ::Datadog::AppSec::Contrib::Devise::Patcher::RememberablePatch#validate and ::Devise::Strategies::Authenticatable#__validate_datadog_authenticatable the ancestor chain of calls is broken: Ruby "reenters" the instance and starts a new ancestor chain of calls, which creates the opportunity for infinite recursive looping.

I would favour a method where the patches collaborate through context passing: RememberablePatch sets a flag in a context, and AuthenticatablePatch checks for that flag.

Copy link
Member Author

@y9v y9v Sep 5, 2024

Choose a reason for hiding this comment

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

Thank you! It is indeed better to use some variable for context. Fixed in 8e60850

@y9v y9v force-pushed the appsec-avoid-tracking-devise-rememberable-logins branch from 6aeede4 to c65e4b0 Compare September 5, 2024 14:05
@y9v y9v requested a review from lloeki September 5, 2024 15:29
@y9v y9v force-pushed the appsec-avoid-tracking-devise-rememberable-logins branch from c65e4b0 to fa52fd2 Compare September 9, 2024 10:02
@github-actions github-actions bot requested a review from a team as a code owner September 9, 2024 10:35
@y9v y9v requested a review from Strech September 10, 2024 09:19
Copy link
Contributor

@Strech Strech left a comment

Choose a reason for hiding this comment

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

Like it!

@y9v y9v force-pushed the appsec-avoid-tracking-devise-rememberable-logins branch from 02d0df4 to 8e60850 Compare September 13, 2024 18:29
@y9v y9v merged commit 7f9ed24 into master Sep 18, 2024
193 checks passed
@y9v y9v deleted the appsec-avoid-tracking-devise-rememberable-logins branch September 18, 2024 13:27
@github-actions github-actions bot added this to the 2.4.0 milestone Sep 18, 2024
@y9v y9v mentioned this pull request Oct 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
appsec Application Security monitoring product integrations Involves tracing integrations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants