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

Add caching? helper method #42365

Merged
merged 1 commit into from
Aug 3, 2021
Merged

Conversation

joelhawksley
Copy link
Contributor

Problem

Caching something that shouldn't be cached is a potential source of bugs and security vulnerabilities. For example, one could write a form helper that outputs a request-specific auth token, only for the helper to be used inside of a cache block.

Proposed solution

In the GitHub application, we implemented a caching? method and used it to raise an error if a specific code path is being cached that we don't want to be cached.

I've credited its original author, @btoews.

Co-authored-by: Ben Toews mastahyeti@gmail.com
Co-authored-by: John Hawthorn jhawthorn@github.com

@rails-bot rails-bot bot added the actionview label Jun 2, 2021
@eileencodes
Copy link
Member

I've been thinking about this PR and I'm not entirely convinced it's necessary or useful to other apps. If the problem is that we don't want to cache sensitive data then we shouldn't add caching blocks for that. Am I missing a piece of how this is used that results in sensitive data getting cached accidentally? The code author would still need to know what data is considered sensitive and need to know to raise if there was sensitive data they didn't want cached.

@rafaelfranca
Copy link
Member

I think one of the problem is that something people don't know they are inside a cached template/partial but this would still be true even if we add this method. People would still accidentally add sensitive data inside cached blocks. This helper only makes easy to opt-out of the cache fragment and instead of having two different templates for a cached partial and an uncached partial, you only have one. I'm not sure it is worth a fiber-local.

@joelhawksley
Copy link
Contributor Author

@eileencodes @rafaelfranca thanks for taking the time to review. I'll provide a more detailed example to explain why I think this feature is generally useful.

We have a custom form helper (let's call it button_form) for building buttons that are forms. Part of this helper includes building CSRF tokens. In order to prevent developers from accidentally using button_form inside a cache block, we raise an error if we're currently caching:

# Build a button that is a form
#
# Returns an HTML String.
def button_form(name, options = {}, html_options = {})
  # Ensure that we aren't caching CSRF tokens.
  raise NoCachingError if caching?

  # build the form, including CSRF tokens
end

At least in our application, it can be difficult to reason about the render stack to be certain that a given line of code won't execute inside a cache block. I think this kind of helper is a good tool to have for building safety mechanisms like the given example.

@rafaelfranca
Copy link
Member

We don't use much fragment cache for me to care about this solution, but if others think it is fine it is ok to me. About the implementation, I'd prefer if we add a Runtime Registry in Action View using ActiveSupport::PerThreadRegistry instead of using the fiber local directly.

@kaspth
Copy link
Contributor

kaspth commented Jun 22, 2021

Could also consider adding an uncachable! method to auto-raise that NoCachingError so users wouldn't have to define their own exception. Think we could use __callee__ within the exception to extract e.g. button_form for the message.

@joelhawksley
Copy link
Contributor Author

@rafaelfranca @kaspth thanks for the reviews. I rewrote this PR to use ActiveSupport::PerThreadRegistry.

@kaspth what do you think about having both caching? and uncachable!?

@joelhawksley
Copy link
Contributor Author

@kaspth friendly bump 😄

@kaspth
Copy link
Contributor

kaspth commented Jul 28, 2021

@kaspth what do you think about having both caching? and uncachable!?

Sorry I wasn't clear, I meant that we'd have both yeah.

Copy link
Contributor

@kaspth kaspth left a comment

Choose a reason for hiding this comment

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

Sweet, getting there!

Let's just define the CachingRegistry inline in CacheHelper right below the private marker. It doesn't warrant being a top-level file at the action_view/lib/* root.

# frozen_string_literal: true

module ActionView
# = Action View Caching registry
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this comment when the registry is nodoc'ed?

actionview/lib/action_view/helpers/cache_helper.rb Outdated Show resolved Hide resolved
actionview/lib/action_view/helpers/cache_helper.rb Outdated Show resolved Hide resolved
actionview/lib/action_view/helpers/cache_helper.rb Outdated Show resolved Hide resolved
actionview/lib/action_view/caching_registry.rb Outdated Show resolved Hide resolved
@joelhawksley joelhawksley force-pushed the caching-predicate branch 2 times, most recently from ce8fcd9 to faf1aa2 Compare July 29, 2021 22:12
@joelhawksley joelhawksley marked this pull request as draft July 29, 2021 22:37
Copy link
Contributor

@kaspth kaspth left a comment

Choose a reason for hiding this comment

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

Looks like the changelog entry just needs to be moved and then we could ship this now — we can also tackle the uncachable! method here if you'd want.

actionview/CHANGELOG.md Outdated Show resolved Hide resolved
@joelhawksley joelhawksley force-pushed the caching-predicate branch 2 times, most recently from 3a14362 to e920dec Compare August 3, 2021 21:47
Caching something that shouldn't be cached is a potential source of
bugs and security vulnerabilities. For example, one could write a
form helper that outputs a request-specific auth token, only for
the helper to be used inside of a `cache` block.

In the GitHub application, we implemented a caching? method and used
it to raise an error if a specific code path is being cached that
we don't want to be cached.

I've credited its original author, @btoews.

Co-authored-by: Ben Toews <mastahyeti@gmail.com>
Co-authored-by: John Hawthorn <jhawthorn@github.com>
Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>
Copy link
Contributor

@kaspth kaspth left a comment

Choose a reason for hiding this comment

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

Rad!

@@ -0,0 +1,3 @@
<%= cache "uhoh" do %>
Copy link
Contributor

Choose a reason for hiding this comment

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

uhoh indeed, love it 😂

@kaspth kaspth marked this pull request as ready for review August 3, 2021 22:14
@kaspth kaspth merged commit c85d24c into rails:main Aug 3, 2021
@kaspth
Copy link
Contributor

kaspth commented Aug 3, 2021

I know it was marked as a draft PR, but I couldn't see anything else that was missing, so I went ahead. If that was erroneous, I'll be around so we can tackle it in a follow up PR. Thanks for working on this, I'm really happy with how it turned out! ❤️

@joelhawksley
Copy link
Contributor Author

@kaspth Ha! I was just waiting for CI to pass before moving it out of draft.

Thanks for your help landing this PR, I really appreciate your thoughtful feedback ❤️

@joelhawksley joelhawksley deleted the caching-predicate branch August 3, 2021 22:26
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.

4 participants