Skip to content
This repository

Mask CSRF tokens to mitigate BREACH attack #11729

Open
wants to merge 5 commits into from

16 participants

Bradley Buda Egor Homakov Adrien Lamothe Wayne Robinson James Coglan Steve Klabnik Michael Koziarski Ben Toews Neal Harris Jon Rowe Sytse Sijbrandij Eugene Gilburg Justin Mazzi Tony Arcieri Matt Silverlock Robin Dupret
Bradley Buda

The BREACH attack described at Black Hat this year allows an attacker to recover plaintext from SSL sessions if they have some idea what they're looking for. One high-value thing to steal that has a predictable plaintext format is the CSRF token (because it always appears in a meta tag and frequently in form tags as well).

The researchers who discovered the attack suggest mitigating it by "masking" secret tokens so they are different on each request. This implements their suggested masking approach from section 3.4 of the paper (PDF). The authenticity token is delivered as a 64-byte string, instead of a 32-byte string. The first 32 bytes are a one-time pad, and the second 32 are an XOR between the pad and the "real" CSRF token. The point is not to hide the token from the client, but to make sure it is different on every request so it's impossible for an attacker to recover by measuring compressability.

The code should be backwards-compatible with existing Rails installs; the format of session[:_csrf_token] is unchanged, and unmasked tokens will still be accepted from clients (with a warning) so that you don't invalidate all your users' sessions on deploy. However, if users have overridden ActionController#verfied_request?, this may break them (depending on whether or not they're calling super).

This is not a blanket fix for BREACH, just a way of protecting against one particular variant of attack. I am not a security expert; I've just implemented the fix as suggested in the paper. This should be reviewed by someone who knows what they're doing.

actionpack/lib/action_controller/authenticity_token.rb
... ... @@ -0,0 +1,54 @@
  1 +module ActionController
  2 + class AuthenticityToken
  3 + class << self
1
Eugene Gilburg
egilburg added a note

I wouldn't make this a use class methods. It seems most of the methods here revolve around the session object, and thus it would make sense to make AuthenticityToken be initialized with a readable attribute session as follows:

token = AuthenticityToken.new(session)
token.generate_masked

# ...

class AuthenticityToken
  attr_reader :session

  def initialize(session)
    @session = session
  end

  def generate_masked
    # ...
  end

  # ...
end

On the same topic, based on the Single Responsibility Principle, perhaps you could make different classes for the token generator and the token itself:

token_builder = AuthenticityTokenGenerator.new(session)
token = token_builder.generate_masked

Of course, for consumers who only need the token object iself, the above can be simplified to:

token = AuthenticityTokenGenerator.new(session).generate_masked  # => an AuthenticityToken object

Or an even simpler, with a class builder method:

token = AuthenticityTokenGenerator.generate(session) # => an AuthenticityToken object

This way, the methods related to generating the token can be on the builder class, while the methods related to validating it can be on the token class itself.

The xor_byte_strings method can still be a class method because it's accessed both by builder and by token itself, and does not operate on a session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_controller/authenticity_token.rb
... ... @@ -0,0 +1,54 @@
  1 +module ActionController
  2 + class AuthenticityToken
  3 + class << self
  4 + LENGTH = 32
  5 +
  6 + def generate_masked(session)
  7 + one_time_pad = SecureRandom.random_bytes(LENGTH)
  8 + encrypted_csrf_token = xor_byte_strings(one_time_pad, master_csrf_token(session))
  9 + masked_token = one_time_pad + encrypted_csrf_token
1
Eugene Gilburg
egilburg added a note

Consider using .concat here instead of + to avoid extra object creation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_controller/authenticity_token.rb
... ... @@ -0,0 +1,54 @@
  1 +module ActionController
  2 + class AuthenticityToken
  3 + class << self
  4 + LENGTH = 32
  5 +
  6 + def generate_masked(session)
  7 + one_time_pad = SecureRandom.random_bytes(LENGTH)
  8 + encrypted_csrf_token = xor_byte_strings(one_time_pad, master_csrf_token(session))
  9 + masked_token = one_time_pad + encrypted_csrf_token
  10 + Base64.strict_encode64(masked_token)
  11 + end
  12 +
  13 + def valid?(session, encoded_masked_token, logger = nil)
  14 + return false if encoded_masked_token.nil?
1
Eugene Gilburg
egilburg added a note

Since you don't expect false to be passed for encoded_masked_token with different semantics than nil, it's simpler to write:

  return false unless encoded_masked_token
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_controller/authenticity_token.rb
((16 lines not shown))
  16 + masked_token = Base64.strict_decode64(encoded_masked_token)
  17 +
  18 + # See if it's actually a masked token or not. In order to
  19 + # deploy this code, we should be able to handle any unmasked
  20 + # tokens that we've issued without error.
  21 + if masked_token.length == LENGTH
  22 + # This is actually an unmasked token
  23 + if logger
  24 + logger.warn "The client is using an unmasked CSRF token. This " +
  25 + "should only happen immediately after you upgrade to masked " +
  26 + "tokens; if this persists, something is wrong."
  27 + end
  28 +
  29 + masked_token == master_csrf_token(session)
  30 +
  31 + elsif masked_token.length == LENGTH * 2
1
Eugene Gilburg
egilburg added a note

You don't have an else clause here. While it will return nil which acts as falsy, given the boolean method name valid?, it's probably better to explicitly return else false here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_controller/authenticity_token.rb
((19 lines not shown))
  19 + # deploy this code, we should be able to handle any unmasked
  20 + # tokens that we've issued without error.
  21 + if masked_token.length == LENGTH
  22 + # This is actually an unmasked token
  23 + if logger
  24 + logger.warn "The client is using an unmasked CSRF token. This " +
  25 + "should only happen immediately after you upgrade to masked " +
  26 + "tokens; if this persists, something is wrong."
  27 + end
  28 +
  29 + masked_token == master_csrf_token(session)
  30 +
  31 + elsif masked_token.length == LENGTH * 2
  32 + # Split the token into the one-time pad and the encrypted
  33 + # value and decrypt it
  34 + one_time_pad = masked_token[0...LENGTH]
1
Eugene Gilburg
egilburg added a note

You can use masked_token.first(LENGTH) here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_controller/authenticity_token.rb
((20 lines not shown))
  20 + # tokens that we've issued without error.
  21 + if masked_token.length == LENGTH
  22 + # This is actually an unmasked token
  23 + if logger
  24 + logger.warn "The client is using an unmasked CSRF token. This " +
  25 + "should only happen immediately after you upgrade to masked " +
  26 + "tokens; if this persists, something is wrong."
  27 + end
  28 +
  29 + masked_token == master_csrf_token(session)
  30 +
  31 + elsif masked_token.length == LENGTH * 2
  32 + # Split the token into the one-time pad and the encrypted
  33 + # value and decrypt it
  34 + one_time_pad = masked_token[0...LENGTH]
  35 + encrypted_csrf_token = masked_token[LENGTH..-1]
1
Eugene Gilburg
egilburg added a note

You can use masked_token.last(LENGTH) here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_controller/authenticity_token.rb
((30 lines not shown))
  30 +
  31 + elsif masked_token.length == LENGTH * 2
  32 + # Split the token into the one-time pad and the encrypted
  33 + # value and decrypt it
  34 + one_time_pad = masked_token[0...LENGTH]
  35 + encrypted_csrf_token = masked_token[LENGTH..-1]
  36 + csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
  37 +
  38 + csrf_token == master_csrf_token(session)
  39 + end
  40 + end
  41 +
  42 + private
  43 +
  44 + def xor_byte_strings(s1, s2)
  45 + s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
1
Eugene Gilburg
egilburg added a note

Can use map! to avoid extra object creation. Also, don't need parentheses around c1, c2.

If this is a hotspot, can optimize a bit more by extraciting 'c*' into a constant to avoid object creation each time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_controller/authenticity_token.rb
((34 lines not shown))
  34 + one_time_pad = masked_token[0...LENGTH]
  35 + encrypted_csrf_token = masked_token[LENGTH..-1]
  36 + csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
  37 +
  38 + csrf_token == master_csrf_token(session)
  39 + end
  40 + end
  41 +
  42 + private
  43 +
  44 + def xor_byte_strings(s1, s2)
  45 + s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
  46 + end
  47 +
  48 + def master_csrf_token(session)
  49 + session[:_csrf_token] ||= SecureRandom.base64(LENGTH)
1
Eugene Gilburg
egilburg added a note

It doesn't seem worthwhile to pass around the entire session object where most methods (other than this one) just care about the token value itself, not mutating the session. I think that the code mutating the session should live separately from the methods/class that generates/validates the token - as early as possible, the token should be extracted and operated on, and as late as possible the session should be updated with the new token value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...action_controller/metal/request_forgery_protection.rb
((8 lines not shown))
190 190 end
191 191
192 192 # Sets the token value for the current session.
193 193 def form_authenticity_token
194   - session[:_csrf_token] ||= SecureRandom.base64(32)
  194 + AuthenticityToken.generate_masked(session)
2
Eugene Gilburg
egilburg added a note

I wonder if this will impact performance. This method, and all string manipulation logic inside it, will be executed on every request (as opposed to current implementation, which caches the value in the session). Perhaps add some benchmarks to this method?

Adrien Lamothe
Alamoz added a note

Why not make it a configurable option, so users can decide which is more important to them?

config.csrf_masking - or - config.mask_csrf

The first form, though longer in length, hints the process recurs for each request. Perhaps also add a comment about decreased performance if decrease is significant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Egor Homakov

it's SSL problem, isn't it. Can you imagine hassle of generating new token everytime?

Adrien Lamothe

Doesn't look too bad. He is implementing the suggested solution.

Wayne Robinson

Would this have the side-effect of also protecting encrypted cookie sessions from this exploit as the ciphertext of the session cookie (containing the _csrf_token) would end up being different on every request too?

Wayne Robinson

After thinking about it, wouldn't just adding random characters to the session every request (when using encrypted sessions) mitigate this attack entirely for all data in the session?

Actually, on further reflection, would I be right in assuming that the session isn't at risk at all because HTTP headers aren't compressed?

I guess this still effects the _csrf_token included as a meta tag in the response body.

Adrien Lamothe

The paper states HTTP-level compression as one of the preconditions. That is something users may configure their web servers to do, so such users may want this proposed behavior in their rails apps.

James Coglan

I would be worth having the masking/unmasking functions available as a library interface rather than baked into AuthenticityToken since users probably have other data they would like to protect using this technique.

James Coglan

@waynerobinson The ciphertext of cookie-stored sessions is different every time anyway, because it uses a random IV during encryption. If you encrypt the same plaintext twice, you should get a different result each time since sending the same ciphertext over the wire tells the attacker that you sent the same data twice, which leaks information.

Steve Klabnik
Collaborator

Calling in the @NZKoz bat signal.

Egor Homakov

We need something universal, maybe we should transfer csrf token as an additional cookie readable on client side.

var token=$.cookie('csrf_token')

It will be a header, not included in response body

Bradley Buda

@waynerobinson Nothing in the HTTP headers is at risk because they're not in the same compression context as the body (you're right; they're not compressed at all).

@homakov Not sending the CSRF token each time would definitely mitigate the attack (or sending it in a header). The problem is that this will break non-XHR form posts, which include the CSRF token as the authenticity_token. A hybrid option would be to send the CSRF token unmasked in an HTTP header for XHRs to use, and to send a masked token in any <form> tags that require it.

Bradley Buda

@egilburg Thanks for the comments; I'll make these changes. I'll also do some benchmarking; we definitely need to figure out if this is expensive or not. If the masking is cheap, I'd just as soon do it on every request and not make it a configuration option (or at least make it opt-out) - users should not want to opt out of masking unless there's a performance hit, or they're doing something really exotic / custom with CSRF tokens.

Bradley Buda

One other option to improve performance / reduce complexity is to use a different technique to obfuscate the authenticity token. There's nothing special about the OTP / XOR masking algorithm suggested in the paper; we just need a way to randomly obfuscate the data on each request in a way that the server can check. So something like salt + MD5(salt+secret) would work, or even bcrypt with the minimum cost. This way I'm not adding random amateur crypto code to Rails and we get an algorithm with predictable performance.

Egor Homakov

Please have a look, this should be a faster and better protection https://gist.github.com/homakov/6147227

Egor Homakov

non-XHR form posts, which include the CSRF token as the authenticity_token

yes, if JS is off we need something different

James Coglan

You can't implement CSRF tokens as a cookie; the weaknesses of cookies is what CSRF tokens are supposed to prevent. If you send the token as a cookie, the browser will attach it to all requests to your server regardless of which origin they come from, defeating the point of CSRF protection.

Plus, any security solution that relies on JavaScript should be considered a weak solution.

James Coglan

If you're concerned about the performance of XOR (which might be a reasonable concern but someone should benchmark it and find out), then write it in C.

Louis Mullie louismullie referenced this pull request in rkh/rack-protection
Open

Mask CSRF tokens to mitigate BREACH attack #64

Egor Homakov

@jcoglan i know how CSRF works at my fingertips :) You probably misunderstood what I proposed: instead of plain tag we put it into Set-Cookie and only after page load (in runtime) we add CSRF tokens and other important information.

Plus, any security solution that relies on JavaScript should be considered a weak solution.

Very foggy argument, what exactly is wrong about this one?

Besides XORing we need a way to hide ANY secret tokens (api_key in my demo). How are you going to hide it? un XORing with javascript? Set-Cookie is a simplest solution, with only one weakness - it requires JS on.

James Coglan

Any solution that assumes the user agent will run JavaScript as part of the security process cannot be general-purpose, since not all user-agents run JavaScript. Invoking the user agent's JS runtime also means there's another component we need to place our trust in.

XOR-masking seems like a totally reasonable approach. It's easily understood and easy to implement, and does not rely on client-side behaviour in order to protect the server. It's the same technique used by WebSocket on all data to prevent the client from constructing arbitrary byte sequences.

The argument that XOR is slow has not been tested, and we should not make arguments from performance when we have no numbers to talk about.

The JS solution may seem to require less code, but it's most complex, since it invokes more parts of the stack in order to work.

We could protect any token by providing two server-side functions, mask(string) and unmask(string). You do not need to unmask these values on the client side; your JS code often does care what their values actually are, it just passes them back to your server and you can unmask the values on the server side.

All I'm saying is we should not lean on JavaScript when a workable solution can be done entirely server-side.

Bradley Buda

Here's a benchmark (let me know if you see a way to improve any of those snippets). Results:

                  user     system      total        real
xor           0.000000   0.000000   0.000000 (  0.000090)
salt + sha1   0.000000   0.000000   0.000000 (  0.000049)
salt + md5    0.000000   0.000000   0.000000 (  0.000038)
bcrypt        0.000000   0.000000   0.000000 (  0.001115)

In absolute terms, everything but Bcrypt is quite fast.

Egor Homakov

All I'm saying is we should not lean on JavaScript when a workable solution can be done entirely server-side.

OK, I agree that XOR suits better for production because my idea requires JS.

You do not need to unmask these values on the client side; your JS code often does care what their values actually are

CSRF token is only an example, to show a developer his api_key or credit card number you will need unmask in JS. (and you will have to lean on JavaScript).

Both solutions are not perfect but XOR covers more cases

Adrien Lamothe

@bradleybuda I've run one of my rails apps in 4.1.0-beta, both with and without your patch, and see no noticeable difference in performance when rendering a form (rendered 22 times for each scenario.) Your benchmark results substantiate this, the time differences are negligible. Here are gists of each test (the times are slow overall because they were run on a laptop, my production server takes 12-20ms to render that page):

Without patch: https://gist.github.com/Alamoz/6148578

With patch: https://gist.github.com/Alamoz/6148540

Michael Koziarski
Owner

I'll be honest and say that most of this complexity strikes me as unnecessary. We're all already disabling TLS compression due to CRIME right? To me this is just another reason to do so?

But most importantly, the BREACH attack was only revealed recently and has barely been reviewed outside of the original researchers and their immediate colleagues. The proposed mitigations may themselves have other implications and may either introduce other issues or not actually fix the underlying problem. Let's let this stew for a while with security researchers doing their analysis on various approaches and wait and see what the security community as a whole recommends.

There's certainly no reason for us to rush to have a fix in before a week has even passed, the odds of us doing it right are basically zero.

One thing we should do though is make sure any SSL/TLS documentation we have strongly recommends disabling compression.

Adrien Lamothe

@NZKoz :+1:

Good to have this PR is place, though, for possible future use. I'll check the documentation and look for a good place to recommend disabling compression.

Wayne Robinson

@NZKoz disabling TLS compression mitigates against CRIME.

BREACH uses a similar method to attack compressed HTTP bodies within the TLS stream.

Not compressing HTTP bodies will have a significant performance impact on a majority of applications as both HTML & JSON have a very large amount of repetition.

The attack relies completely on the data you are trying to locate not changing from one request to the next. If any of this sensitive data varies from one request to the next, guessing the correct value becomes impossible because you never make any headway on your brute force attack.

If we every want to be able to compress anything in our TLS stream, information that a hacker may find useful must be obfuscated uniquely in every request. This doesn't have to be cryptographic, it just needs to add enough extra entropy to the value to make it impossible to know what it will be from request-to-request.

Egor Homakov

1) it must be a separate helper, not just AuthenticityToken
2) for https websites only, no reason to mess with http
3) @NZKoz

has barely been reviewed outside of the original researchers and their immediate colleagues

the idea of the hack sounds pretty clear and feasible, I see no reason to wait for something. CRIME is a real thing, BREACH is just a version of CRIME.

Adrien Lamothe

@homakov :+1:

But also agree with @NZKoz that further study is good. When he says "Let's let this stew for a while..." I'm hoping he means to add the patch before the 4.1 release.

@waynerobinson :+1: Yeah it is nice to be able to utilize compression.

Michael Koziarski
Owner

Yeah, while I may sound like I'm pouring cold water on the work here, I'm really just suggesting that we wait a few months before doing anything as ideally there'll be a clear mitigation strategy available to us in that timeframe. And I do hope that's before the 4.1 release goes out, yes.

The other option may be simply randomizing the order of these tags and shuffling the order of their attributes:

          [
            tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token),
            tag('meta', :name => 'csrf-token', :content => form_authenticity_token)
          ].join("\n").html_safe

That would cause the target to bounce around a bit in the request, however I'm sure that that could be overcome with additional requests.

Wayne Robinson

@NZKoz adding one bit of entropy is hardly worth the effort, especially if the attacker knows they are attacking a Rails site with this addition.

The mitigation options are quite well known as they are understood from CRIME and it is effectively the same attack.

This is a security update and should ideally be implemented into the 4.0 and 3.2 (and earlier) branches.

Bradley Buda bradleybuda Add a ctor to AuthenticityToken and change class methods to instance
Do all the session manipulation in the constructor, and just operate
on a copy of the CSRF token in the instance methods. Slightly
simplifies the controller mixin code and makes the API read a bit
better.
f57295f
Bradley Buda

Updated with some of the refactoring and perf improvements suggested by @egilburg. As for whether or not this change should wait, I'm on the fence; BREACH is a very feasible attack today on real web apps given the amount of detail in the paper (and will only get easier when the PoC code goes out next week). I'm hopeful that the community will find a better mitigation strategy than this, but I don't see an obvious path forward.

Practically, there isn't a big downside to merging this patch; it can be reverted later, and there doesn't seem to be a large performance impact.

Michael Koziarski
Owner

@bradleybuda there's a substantial downside to prematurely merging a patch, it risks lulling people into a false sense of security. "oh, it's fixed" when in reality the fixes haven't yet been reviewed or analyzed sufficiently for us to be confident.

@waynerobinson There's no practical reason to enable compression over TLS because chrome was the only browser to ever implement it in a shipping application, and it has been disabled for some time. Firefox 10 (or maybe 11) supported SPDY compression over TLS but that was removed in the next release.

Again, I'm not opposed to making changes here, and this approach seems pretty reasonable. However the only complete fix for all applications is to disable compression for SSL/TLS, we might fix our csrf tokens but apps may still embed other information which could be vulnerable to the same traffic analysis.

Michael Koziarski
Owner

A more detailed reference for my statement about it being not-relevant to enable TLS compression, the fixed browser versions are:

  • All versions of Internet Explorer (No Versions of IE support SSL Compression)
  • Google Chrome 21.0.1180.89
  • Firefox 15.0.1
  • Opera 12.01
  • Safari 5.1.7 on Windows
  • Safari 5.1.6 & 6 on OSX Lion

Additionally the BREACH website itself points to this I quoted the wrong section entirely:

By disabling TLS/SSL-level compression – which was already little-used, and in fact disabled in most browsers – the attack as demonstrated at ekoparty is completely mitigated.

So while I'm open to doing something here, I'm definitely not ok with doing something hasty

Wayne Robinson

I would like to reiterate that disabling compression for SSL/TLS is not enough.

Disabling compression for SSL/TLS is how you mitigate against CRIME.

If the body of your HTTP requests is compressed (using either the Rails built-in method or via standard compression support from Nginx/Apache) then it is exposed to BREACH.

Michael Koziarski
Owner

Ah, I see. You're right there @waynerobinson

Michael Koziarski
Owner

So I'd suggest at the very least we split this into two chunks.

  1. refactorings to make it simpler to plug in a gem which changes the way that csrf tokens are verified and output.
  2. a patch and gem which enables the masking discussed here an option.

the first change should really be as simple as breaking up verified_request? to have a separate method which does the equality check.

The second one would allow us to ship a mitigation for 4.0.x (in the form of a separate gem) and then eventually bake that in to 4.1

Wayne Robinson

@NZKoz also, if you look at that same BREACH website it lists the following options to mitigate the attack.

  1. Randomizing secrets per request
  2. Masking secrets (effectively randomizing by XORing with a random secret per request)

For the CSRF tokens in Rails, we can do either effectively. It's really a matter of choice.

For a pretty good laymen's description of BREACH I would recommend reading the Ars Technica article (http://arstechnica.com/security/2013/08/gone-in-30-seconds-new-attack-plucks-secrets-from-https-protected-pages/).

After reading this you'll see how any kind of even trivial obfuscation of the CSRF (or other sensitive data in your responses) effectively mitigates this attack.

For example, if you have a 30 character string that get's XORed with a 30-character salt, then a naive brute force attack would take 2**30**30/2 requests to guess this result.

Wayne Robinson

@NZKoz there is a gem for this and the randomised length mitigation at https://github.com/meldium/breach-mitigation-rails which @bradleybuda created as a basis for this PR.

Michael Koziarski
Owner

We can't actually randomize secrets per request without breaking a lot of stuff people rely on. e.g. having a page open in multiple tabs or making lots of ajax requests without doing careful book-keeping about the current csrf tokens.

The xor mitigation seems like the most sensible approach, and the availability of that gem means that users can get to it today if they need to. Disabling compression remains the most sensible thing to do in the face of this attack as there are likely other issues lurking in there somewhere. The breach guys recommend this approach as their first suggestion.

However that's not a reason for us to not ship a sensible fix. Have any other projects or companies announced plans for mitigating this? I'm loathe to be first, even if we're just doing what the paper said.

Wayne Robinson

Disabling compression is not reasonable as it would result in 10–20x increases in traffic for the main body of applications.

I don't understand the fear of making a decision on implementing a fix for a 0-day security issue.

I'm not suggesting that this actual PR is the best way forward, but implementing a fix for Rails 4 and all commonly-used previous versions is the only responsible course of action for any framework with exposure to this attack vector.

Michael Koziarski
Owner

Remember when we all read about BEAST and rushed to disable every CBC cipher, leaving us running RC4 then it turned out that RC4 was broken in TLS? My only concern to rushing out a release is that we do something equally dumb and end up creating a different problem for our users.

As the paper covers, CSRF tokens are only a portion of the problem here, you can recover anything sensitive.

I'm loathe to be first to move here, there are many other frameworks (hopefully most of them) in many other languages which use CSRF protection in the same way we do. We can roll out fixes as it becomes clear what the consensus is as to the best solution for a generalised framework like Rails.

Wayne Robinson

CSRF tokens are the only piece of sensitive data that Rails generates and is responsible for.

A general obfuscation technique should allow application to other pieces of sensitive data if developers decide to also obscure these. CSRF tokens are important because they allow malicious updates to occur.

We should do what we can to protect Rails users as quickly as possible and then roll-out future hot-fixes as more information comes to light.

CBC was broken and so disabling it was the right move. Just because RC4 was also broken, doesn't mean that the best and safest course of action was to wait until we've discovered all issues before doing anything at all. If that was the case, we would never actually implement anything!

Security is not something we can take a back-seat to and regularly applying security-related hot-fixes is something the communities of all frameworks need to get more used to as the entire Internet becomes more complex.

Michael Koziarski
Owner

@waynerobinson have you actually exploited a rails app using their proof of concept? Looking at the paper in more detail it appears that the way rails embeds the secret is akin to the difficulties they discuss in section 2.4.7?

We embed the csrf_token in the html body, but we don't reflect the token, and a known prefix, in both the request and response? The request contains: request_forgery_protection_token=lolhaxwtfomg but the response will only ever contain either content="lolhaxwtfomg" or value="lolhaxwtfomg", while the value is reflected, there's no reflected prefix so you presumably couldn't bootstrap the process?

It's possible I'm missing something obvious, I have a habit of doing that as I've already shown here :smiley: but at first glance it's at least worth investigation

Wayne Robinson

I have not exploited Rails with their proof of concept however, the discussion paper demonstrates that Rails is directly exposed.

The head contains:

<meta content="{authetoken}" name="csrf-token" />

Rails forms contains:

<input name="authenticity_token" type="hidden" value="{authetoken}">

This seems like a pretty well-known prefixes/postfixes to me. The exploit works either forwards or backwards from either name="csrf-token" or name="authenticity_token".

Most browsers don't compress requests (I can't think of any), so the requests aren't exposed to this threat. But the authenticity_token could be

Michael Koziarski
Owner

I'm not sure if I'm misunderstanding the attack, or you are, but one of us is. In order to expedite things I've pinged the researchers on their contact address, hopefully we can get the final word from them and act accordingly. I can see from the paper that @nealharris is one of the researchers, and works for a company using rails, I'm not sure if he'll get this notification though.

The paper describes how you need to reflect a value the attacker can change in the response body, and contain a known prefix with both the attacker's data and the data you seek to recover. So it's not enough that the csrf token is embedded in every response, you also have to embed the attacker's guess in the response data, and that embedding needs to contain a common prefix.

So if you take a typical rails application, we can rule out responses generated by POST requests as they will require correct CSRF tokens to proceed and otherwise the response will either be an error page (:exception) contain a value which is not the user's csrf_token (:null_session) or cause the session to reset (:reset_session).

Developers are incredibly unlikely to allow attacker-provided data to be put into <meta content=' tags, so in order to be vulnerable I'm assuming we'll need:

  1. A GET request or some other request which the developer has disabled CSRF protection on
  2. The response to that request must to generate a form which contains one of the attacker-provided parameters as the value of an input tag with no modification.
  3. That response must also generate a form with a POST action ensuring that the CSRF token is embedded as the value of an input tag with no modification.
  4. The size of that response must not vary based on the attacker-provided parameters

I suppose this could happen if the application has all of the following:

  • a search form which accepts GET requests
  • the attacker can generate a query for that form which they know will never return any results (assuming the 'no results' page is static)
  • the search form is rendered with all the attacker's relevant search params
  • It also contains an additional param which the attacker uses as his guessing parameter
  • the response also contains a non-ajax POST form. e.g. a login form

This is certainly plausible, especially applications with admin interfaces which allow faceted searching. But it hardly describes every application under the sun? When you add in the requirement for an MITM situation it gets harder still.

I agree with need to ship a mitigation for this, but the first rule of fixing security issues is you fix them correctly and carefully, not in a rush using some idea you found on the internet.

Finally, let's not lecture one another else on the correct way of handling bugs or issues, we're all on the same team here and want the same result. A secure framework. Let's focus on the issues and the fix and not histrionics.

Wayne Robinson

I'm definitely not suggesting we implement a crappy solution. But from your initial comments it seemed like you were advocating for a a fix that would get added to Rails 4.1 when that is released, rather than something that is important to back-port to as many previous Rails versions as practical.

I may have misunderstood the requirement to allow injection of the value the attacker is attempting to guess. If this is the case, I'm not sure how this would be exploited in the majority of situations and I apologise for my histrionics. :-)

To mitigate the GET-based attack vector, should we not consider never allowing the CSRF to be overridden (and is it even possible to do so)? I can't think of any reasons why this should be generated any other way than directly from the server.

So if this is the case, then the CSRF protection with Rails isn't at risk and only the actual value of parameters passed into a GET request is ever at risk of CRIME.

If this is the case, I'm not sure how any Rails developer (following REST-standards) could be concerned. I can't think of much that would be passed into a GET request that would be confidential. We have to be missing something right?

Michael Koziarski
Owner

It's not that the attacker can control the csrf token, but that he can control another parameter that ends up in the form input tags. By doing that, and knowing that the response will also contain the csrf_token the attacker is comparing the effects of compression on:

  <input type='hidden' name='who_cares_but_not_the_token' value='asdfHACKERGUESS' />
  <input type='hidden' name='form_authenticity_token' value='asdfACTUALSECRET' />

That gives them a common prefix value=' with which to bootstrap the guessing. Without both of those being in the response, and there being no CSRF protection on the request itself, then there's no way that his guesses will cause the DEFLATE compression to change the size of the response in a dependable way. There's also the other option that the DEFLATE compression doesn't actually use value=' as it finds other, longer, common strings.

We'll definitely fix this in 4.1, we'll possibly backport that fix to earlier releases, we might publish an advisory. It's all a question of how exploitable this is, the severity, the risk of the fix etc.

Normally these discussions happen behind closed doors and a patch and advisory lands in people's inboxes, however as this is a general issue and disclosed to the public already, we can be a little more open than usual in discussing it. I'm sure our friends at NSA have already been using this for years ;)

Ben Toews

From what I have read about the attack, it doesn't seem like it should require the attacker controlled string to be reflected in an input tag. Couldn't the attacker go after the meta tag directly by starting with " name="csrf-token" /> and working backwards, guessing the last character of the token first?

Michael Koziarski
Owner

@mastahyeti I think that would require the attacker to be able to embed " and > in the responses, which are escaped due to XSS concerns.

So if the app has an XSS vulnerability in response to a single get request then it could work backwards like that, but an app with an XSS vulnerability doesn't need a CSRF token to be recovered as the attacker could just trick the user into clicking that link and steal what they want from there.

Ben Toews

Good point. They would at least need ".

Michael Koziarski
Owner

probably more than " as two characters are unlikely to have a measurable impact on the DEFLATE processing.

Wayne Robinson

Could probably squeeze =" name="csrf-token" / in somewhere in the output though.

Ben Toews

I just meant that they wouldn't need the > character. If there was some context where quotes weren't escaped for XSS, they could start with " name="csrf-token". html_escape and escape_javascript will both munge quotes though.

With that in mind though, another mitigating factor is that there are likely to be many value= and content= strings on any given page, making it very difficult for the attacker to isolate the tag containing the CSRF token if we are assuming they only control the value of the value attribute of some tag.

Eugene Gilburg egilburg commented on the diff
actionpack/lib/action_controller/authenticity_token.rb
... ... @@ -0,0 +1,62 @@
  1 +module ActionController
  2 + class AuthenticityToken
  3 + LENGTH = 32
  4 +
  5 + # Note that this will modify +session+ as a side-effect if there is
  6 + # not a master CSRF token already present
  7 + def initialize(session, logger = nil)
  8 + session[:_csrf_token] ||= SecureRandom.base64(LENGTH)
  9 + @master_csrf_token = Base64.strict_decode64(session[:_csrf_token])
  10 + @logger = logger
2
Eugene Gilburg
egilburg added a note

Would be friendly to make logger an attr_accessor so other code/tests can get/set it post-initialize.

Justin Mazzi
jmazzi added a note

Unless it's being used outside this class right now, I don't think that is needed, personally :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eugene Gilburg egilburg commented on the diff
...action_controller/metal/request_forgery_protection.rb
@@ -203,5 +203,9 @@ def form_authenticity_param
203 203 def protect_against_forgery?
204 204 allow_forgery_protection
205 205 end
  206 +
  207 + def authenticity_token
  208 + AuthenticityToken.new(session, logger)
1
Eugene Gilburg
egilburg added a note

You don't need to generate this over and over in a single request, right? Please memoize this e.g. @authenticity_token ||= AuthenticityToken.new(session, logger), so this doesn't generate several times between calls in the same request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eugene Gilburg egilburg commented on the diff
actionpack/lib/action_controller/authenticity_token.rb
((27 lines not shown))
  27 + masked_token = Base64.strict_decode64(encoded_masked_token)
  28 +
  29 + # See if it's actually a masked token or not. In order to
  30 + # deploy this code, we should be able to handle any unmasked
  31 + # tokens that we've issued without error.
  32 + if masked_token.length == LENGTH
  33 + # This is actually an unmasked token
  34 + if @logger
  35 + @logger.warn "The client is using an unmasked CSRF token. This " +
  36 + "should only happen immediately after you upgrade to masked " +
  37 + "tokens; if this persists, something is wrong."
  38 + end
  39 +
  40 + masked_token == @master_csrf_token
  41 +
  42 + elsif masked_token.length == LENGTH * 2
4
Eugene Gilburg
egilburg added a note

Might seem like a micro-optimization, but also for slightly better readability, I'd rename the constant LEGNTH to UNMASKED_LENGTH (or RAW_LENGTH), and right below it define MASKED_LENGTH = UNMASKED_LENGTH * 2. Then I'd just be checking against the two constants here. Also, make it a case statement and put the more commonly expected case further up:

case masked_token.length
when MASKED_LENGTH
  # ...
when UNMASKED_LENGTH
  # ...
else
  false
end
Steve Klabnik Collaborator

case statements return nil which is falsy if none of the whens happen so you wouldn't need the else clause.

Eugene Gilburg
egilburg added a note

Do you think it's good practice for Boolean methods to return explicit true or false, not just truthy or falsy?

Steve Klabnik Collaborator

I think it's actively bad practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Neal Harris

One thing to note regarding bootstrapping: only three characters are necessary to get it going. Consider

<meta content="csrf_token_which_is_base64_encoded=" name="csrf-token" />

Note that if an attacker's query param is reflected in an attribute value anywhere in the page as in

<input type="hidden" name="who_cares_but_not_the_token" value="asdfHACKERGUESS" />

then the ="<FIRST_CHAR_OF_HACKER_GUESS> in value="asdfHACKERGUESS" will match against the ="<FIRST_CHAR_OF_ACTUAL_SECRET> in content="csrf_token_which_is_base64_encoded=" when the guess is correct. The actual name of the attribute doesn't have to match. This is likely to be noisy for the attacker, given how many other places in the page they might be colliding with. Worth mentioning though.

Also, I emphasize b64 encoding here, since that gives the attacker some idea of how the secret will end, and allows them to use the same idea to bootstrap from the end, instead of the beginning.

Michael Koziarski NZKoz commented on the diff
actionpack/lib/action_controller/authenticity_token.rb
((22 lines not shown))
  22 + end
  23 +
  24 + def valid?(encoded_masked_token)
  25 + return false unless encoded_masked_token
  26 +
  27 + masked_token = Base64.strict_decode64(encoded_masked_token)
  28 +
  29 + # See if it's actually a masked token or not. In order to
  30 + # deploy this code, we should be able to handle any unmasked
  31 + # tokens that we've issued without error.
  32 + if masked_token.length == LENGTH
  33 + # This is actually an unmasked token
  34 + if @logger
  35 + @logger.warn "The client is using an unmasked CSRF token. This " +
  36 + "should only happen immediately after you upgrade to masked " +
  37 + "tokens; if this persists, something is wrong."
2
Michael Koziarski Owner
NZKoz added a note

This doesn't actually matter, if an attacker has the raw token, they can generate a valid masked token, I think we can safely ignore it rather than log.

Egor Homakov
homakov added a note

yes, such a notification is very rare. only for those who opened page just before deployment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Egor Homakov

discussion above is pretty long but @NZKoz is right, w/o reflection it doesn't work! you need it to put guesses.
Although it's very easy to find a reflection. Search etc

Neal Harris

One more comment: when we were doing our research, we definitely took a look at Rails to see how easy/difficult the attack would be there. It didn't make life easy for us (for essentially the reasons discussed above), and wouldn't have been a good candidate for a demo. That being said, as I think everyone here realizes: Rails is vulnerable in in principle.

Anyway, the fact that @angeloprado, @ygluck, and I didn't find a good way to attack Rails shouldn't provide too much comfort. There are lots of people out there that are way more industrious and clever than we are.

Schuyler Cebulskie Gawdl3y referenced this pull request in laravel/framework
Closed

Mask CSRF tokens to mitigate BREACH attack #2046

acorncom acorncom referenced this pull request in yiisoft/yii
Open

Mitigate BREACH attack possibilities #2819

Tony Arcieri tarcieri commented on the diff
actionpack/lib/action_controller/authenticity_token.rb
((25 lines not shown))
  25 + return false unless encoded_masked_token
  26 +
  27 + masked_token = Base64.strict_decode64(encoded_masked_token)
  28 +
  29 + # See if it's actually a masked token or not. In order to
  30 + # deploy this code, we should be able to handle any unmasked
  31 + # tokens that we've issued without error.
  32 + if masked_token.length == LENGTH
  33 + # This is actually an unmasked token
  34 + if @logger
  35 + @logger.warn "The client is using an unmasked CSRF token. This " +
  36 + "should only happen immediately after you upgrade to masked " +
  37 + "tokens; if this persists, something is wrong."
  38 + end
  39 +
  40 + masked_token == @master_csrf_token
1
Tony Arcieri
tarcieri added a note

Is this vulnerable to a timing attack?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Tony Arcieri tarcieri commented on the diff
actionpack/lib/action_controller/authenticity_token.rb
((34 lines not shown))
  34 + if @logger
  35 + @logger.warn "The client is using an unmasked CSRF token. This " +
  36 + "should only happen immediately after you upgrade to masked " +
  37 + "tokens; if this persists, something is wrong."
  38 + end
  39 +
  40 + masked_token == @master_csrf_token
  41 +
  42 + elsif masked_token.length == LENGTH * 2
  43 + # Split the token into the one-time pad and the encrypted
  44 + # value and decrypt it
  45 + one_time_pad = masked_token.first(LENGTH)
  46 + encrypted_csrf_token = masked_token.last(LENGTH)
  47 + csrf_token = self.class.xor_byte_strings(one_time_pad, encrypted_csrf_token)
  48 +
  49 + csrf_token == @master_csrf_token
7
Tony Arcieri
tarcieri added a note

Likewise, is this also vulnerable to a timing attack?

Michael Koziarski Owner
NZKoz added a note

timing attacks are unlikely to be an issue here, CSRF tokens are particular to the session, not server side. The CSRF attacker doesn't have the ability to see the requests and time their responses unless you're talking about a fairly specific set of scenarios like an active MITM attacker. I'd love to seem someone demo that as it'd be quite a neat trick, but seems an unlikely vector at present.

We could change it, but it'd be belt-and-suspenders style stuff not a critical fix.

Tony Arcieri
tarcieri added a note

Well, you're trying to mitigate BREACH here, which already requires the attacker MITM with a TCP proxy and can drive the victim's browser. A timing attack on CSRF would have the exact same setup, but rather than using the TCP proxy to measure the length of the response, they'd use it to measure timing information (which, granted, is harder than measuring the number of bytes in the response ;)

Michael Koziarski Owner
NZKoz added a note

Sure, I'm not denying it's theoretically possible, nor rejecting the idea of changing the code. Just not FREAKING OUT at every use of String#== ;).

Tony Arcieri
tarcieri added a note

Note that I am not one to FREAK OUT at every use of String#== either [1] :P But given the attack this patch is intending to mitigate, I think it's warranted

Just playing around a bit, you can get millisecond level timing info on a cross-domain POST, but that obviously isn't enough for a timing attack against string comparisons.

Tony Arcieri
tarcieri added a note

What if the attacker is MitMing you with a TCP proxy (like they would have to do for BREACH anyway?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Tony Arcieri tarcieri commented on the diff
actionpack/lib/action_controller/authenticity_token.rb
((3 lines not shown))
  3 + LENGTH = 32
  4 +
  5 + # Note that this will modify +session+ as a side-effect if there is
  6 + # not a master CSRF token already present
  7 + def initialize(session, logger = nil)
  8 + session[:_csrf_token] ||= SecureRandom.base64(LENGTH)
  9 + @master_csrf_token = Base64.strict_decode64(session[:_csrf_token])
  10 + @logger = logger
  11 + end
  12 +
  13 + def generate_masked
  14 + # Start with some random bits
  15 + masked_token = SecureRandom.random_bytes(LENGTH)
  16 +
  17 + # XOR the random bits with the real token and concatenate them
  18 + encrypted_csrf_token = self.class.xor_byte_strings(masked_token, @master_csrf_token)
3
Tony Arcieri
tarcieri added a note

"encrypted?"

Michael Koziarski Owner
NZKoz added a note

yeah, masked is a better name for it

It may make more sense to rename masked_token to one_time_pad as per #L45.

masked_token is likely a better name for the XOR of the real CSRF token and our one-time pad.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jon Rowe

Hey @bradleybuda have you had time to react to the feedback from @NZKoz and @tarcieri? Seems like this is something important to get fixed...

Neal Harris nealharris referenced this pull request from a commit in square/rails
Neal Harris nealharris mask csrf tokens with otp to prevent BREACH.
essentially copied from rails#11729.
4de343d
Neal Harris nealharris referenced this pull request from a commit in square/rails
Neal Harris nealharris patch for breach; adapted from rails#11729 1d6cf31
Neal Harris nealharris referenced this pull request from a commit in square/rails
Neal Harris nealharris patch for breach; adapted from rails#11729 2d43ecc
Neal Harris nealharris referenced this pull request from a commit in square/rails
Neal Harris nealharris patch for breach. adapted from rails#11729 f73bfe9
Neal Harris nealharris referenced this pull request in square/rails
Merged

patch breach for csrf tokens. #18

Sytse Sijbrandij

For people waiting for this to get merged consider using https://github.com/meldium/breach-mitigation-rails in the meantime

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

Showing 5 unique commits by 1 author.

Aug 03, 2013
Bradley Buda bradleybuda Implement standalone generate and verify methods for masked tokens c3f6880
Bradley Buda bradleybuda RequestForgeryProtection uses Authenticity token for generation and v…
…erification
ce62493
Aug 05, 2013
Bradley Buda bradleybuda Add a ctor to AuthenticityToken and change class methods to instance
Do all the session manipulation in the constructor, and just operate
on a copy of the CSRF token in the instance methods. Slightly
simplifies the controller mixin code and makes the API read a bit
better.
f57295f
Bradley Buda bradleybuda Oops - revert debugging code that I accidentally pushed 8c3e35c
Bradley Buda bradleybuda Remove more debugging code dd11142
This page is out of date. Refresh to see the latest.
1  actionpack/lib/action_controller.rb
@@ -7,6 +7,7 @@
7 7 module ActionController
8 8 extend ActiveSupport::Autoload
9 9
  10 + autoload :AuthenticityToken
10 11 autoload :Base
11 12 autoload :Caching
12 13 autoload :Metal
62 actionpack/lib/action_controller/authenticity_token.rb
... ... @@ -0,0 +1,62 @@
  1 +module ActionController
  2 + class AuthenticityToken
  3 + LENGTH = 32
  4 +
  5 + # Note that this will modify +session+ as a side-effect if there is
  6 + # not a master CSRF token already present
  7 + def initialize(session, logger = nil)
  8 + session[:_csrf_token] ||= SecureRandom.base64(LENGTH)
  9 + @master_csrf_token = Base64.strict_decode64(session[:_csrf_token])
  10 + @logger = logger
  11 + end
  12 +
  13 + def generate_masked
  14 + # Start with some random bits
  15 + masked_token = SecureRandom.random_bytes(LENGTH)
  16 +
  17 + # XOR the random bits with the real token and concatenate them
  18 + encrypted_csrf_token = self.class.xor_byte_strings(masked_token, @master_csrf_token)
  19 + masked_token.concat(encrypted_csrf_token)
  20 +
  21 + Base64.strict_encode64(masked_token)
  22 + end
  23 +
  24 + def valid?(encoded_masked_token)
  25 + return false unless encoded_masked_token
  26 +
  27 + masked_token = Base64.strict_decode64(encoded_masked_token)
  28 +
  29 + # See if it's actually a masked token or not. In order to
  30 + # deploy this code, we should be able to handle any unmasked
  31 + # tokens that we've issued without error.
  32 + if masked_token.length == LENGTH
  33 + # This is actually an unmasked token
  34 + if @logger
  35 + @logger.warn "The client is using an unmasked CSRF token. This " +
  36 + "should only happen immediately after you upgrade to masked " +
  37 + "tokens; if this persists, something is wrong."
  38 + end
  39 +
  40 + masked_token == @master_csrf_token
  41 +
  42 + elsif masked_token.length == LENGTH * 2
  43 + # Split the token into the one-time pad and the encrypted
  44 + # value and decrypt it
  45 + one_time_pad = masked_token.first(LENGTH)
  46 + encrypted_csrf_token = masked_token.last(LENGTH)
  47 + csrf_token = self.class.xor_byte_strings(one_time_pad, encrypted_csrf_token)
  48 +
  49 + csrf_token == @master_csrf_token
  50 +
  51 + else
  52 + # Malformed token of some strange length
  53 + false
  54 +
  55 + end
  56 + end
  57 +
  58 + def self.xor_byte_strings(s1, s2)
  59 + s1.bytes.zip(s2.bytes).map! { |c1, c2| c1 ^ c2 }.pack('c*')
  60 + end
  61 + end
  62 +end
10 actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -185,13 +185,13 @@ def verify_authenticity_token
185 185 # * Does the X-CSRF-Token header match the form_authenticity_token
186 186 def verified_request?
187 187 !protect_against_forgery? || request.get? || request.head? ||
188   - form_authenticity_token == params[request_forgery_protection_token] ||
189   - form_authenticity_token == request.headers['X-CSRF-Token']
  188 + authenticity_token.valid?(params[request_forgery_protection_token]) ||
  189 + authenticity_token.valid?(request.headers['X-CSRF-Token'])
190 190 end
191 191
192 192 # Sets the token value for the current session.
193 193 def form_authenticity_token
194   - session[:_csrf_token] ||= SecureRandom.base64(32)
  194 + authenticity_token.generate_masked
195 195 end
196 196
197 197 # The form's authenticity parameter. Override to provide your own.
@@ -203,5 +203,9 @@ def form_authenticity_param
203 203 def protect_against_forgery?
204 204 allow_forgery_protection
205 205 end
  206 +
  207 + def authenticity_token
  208 + AuthenticityToken.new(session, logger)
  209 + end
206 210 end
207 211 end
105 actionpack/test/controller/authenticity_token_test.rb
... ... @@ -0,0 +1,105 @@
  1 +require 'abstract_unit'
  2 +require 'active_support/log_subscriber/test_helper'
  3 +
  4 +class AuthenticityTokenTest < ActiveSupport::TestCase
  5 + test 'should generate a master token that is random as a side-effect' do
  6 + first_session = {}
  7 + ActionController::AuthenticityToken.new(first_session)
  8 +
  9 + second_session = {}
  10 + ActionController::AuthenticityToken.new(second_session)
  11 +
  12 + refute_equal first_session[:_csrf_token], second_session[:_csrf_token]
  13 + end
  14 +
  15 + test 'should generate a master token that is a 32-byte base64 string' do
  16 + session = {}
  17 + ActionController::AuthenticityToken.new(session)
  18 + bytes = Base64.strict_decode64(session[:_csrf_token])
  19 + assert_equal 32, bytes.length
  20 + end
  21 +
  22 + test 'should generate masked tokens that are 64-byte base64 strings' do
  23 + masked_token = ActionController::AuthenticityToken.new({}).generate_masked
  24 + bytes = Base64.strict_decode64(masked_token)
  25 + assert_equal 64, bytes.length
  26 + end
  27 +
  28 + test 'should save a new master token to the session if none is present' do
  29 + session = {}
  30 + ActionController::AuthenticityToken.new(session)
  31 + refute_nil session[:_csrf_token]
  32 + end
  33 +
  34 + test 'should not overwrite an existing master token' do
  35 + existing = SecureRandom.base64(32)
  36 + session = {:_csrf_token => existing}
  37 + ActionController::AuthenticityToken.new(session)
  38 + assert_equal existing, session[:_csrf_token]
  39 + end
  40 +
  41 + test 'should generate masked tokens that are different each time' do
  42 + session = {}
  43 + first = ActionController::AuthenticityToken.new(session).generate_masked
  44 + second = ActionController::AuthenticityToken.new(session).generate_masked
  45 + refute_equal first, second
  46 + end
  47 +
  48 + test 'should be able to verify a masked token' do
  49 + session = {}
  50 + token = ActionController::AuthenticityToken.new(session)
  51 + masked_token = token.generate_masked
  52 + assert token.valid?(masked_token)
  53 + end
  54 +
  55 + test 'should be able to verify an unmasked (master) token' do
  56 + # Generate a master token
  57 + session = {}
  58 + token = ActionController::AuthenticityToken.new(session)
  59 + assert token.valid?(session[:_csrf_token])
  60 + end
  61 +
  62 + test 'should warn when verifying an unmasked token' do
  63 + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
  64 +
  65 + session = {}
  66 + token = ActionController::AuthenticityToken.new(session, logger)
  67 + token.valid?(session[:_csrf_token])
  68 +
  69 + assert_equal 1, logger.logged(:warn).size
  70 + assert_match(/unmasked CSRF token/, logger.logged(:warn).last)
  71 + end
  72 +
  73 + test 'should reject an invalid unmasked token' do
  74 + session = {}
  75 + token = ActionController::AuthenticityToken.new(session)
  76 + refute token.valid?(SecureRandom.base64(32))
  77 + end
  78 +
  79 + test 'should reject an invalid masked token' do
  80 + session = {}
  81 + token = ActionController::AuthenticityToken.new(session)
  82 + refute token.valid?(SecureRandom.base64(64))
  83 + end
  84 +
  85 + test 'should reject a token from a different session' do
  86 + old_session = {}
  87 + old_masked_token = ActionController::AuthenticityToken.new(old_session).generate_masked
  88 +
  89 + new_session = {}
  90 + new_token = ActionController::AuthenticityToken.new(new_session)
  91 + refute new_token.valid?(old_masked_token)
  92 + end
  93 +
  94 + test 'should reject a nil token' do
  95 + refute ActionController::AuthenticityToken.new({}).valid?(nil)
  96 + end
  97 +
  98 + test 'should reject an empty token' do
  99 + refute ActionController::AuthenticityToken.new({}).valid?('')
  100 + end
  101 +
  102 + test 'should reject a malformed token' do
  103 + refute ActionController::AuthenticityToken.new({}).valid?(SecureRandom.base64(42))
  104 + end
  105 +end
20 actionpack/test/controller/request_forgery_protection_test.rb
@@ -101,9 +101,18 @@ def form_authenticity_param
101 101 # common test methods
102 102 module RequestForgeryProtectionTests
103 103 def setup
104   - @token = "cf50faa3fe97702ca1ae"
  104 + # Pin the RNG to a fixed value to get predictable CSRF tokens
  105 + # HACK Seed in the same value many times b/c the caller is going
  106 + # to modify it.
  107 + one_time_pad = SecureRandom.random_bytes(32)
  108 + SecureRandom.stubs(:random_bytes)
  109 + .returns(one_time_pad.dup)
  110 + .then.returns(one_time_pad.dup)
  111 + .then.returns(one_time_pad.dup)
  112 + .then.returns(one_time_pad.dup)
  113 +
  114 + @token = ActionController::AuthenticityToken.new(session).generate_masked
105 115
106   - SecureRandom.stubs(:base64).returns(@token)
107 116 ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
108 117 end
109 118
@@ -293,10 +302,9 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
293 302 end
294 303
295 304 test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
296   - SecureRandom.stubs(:base64).returns(@token + '<=?')
297 305 get :meta
298 306 assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
299   - assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae&lt;=?'
  307 + assert_select 'meta[name=?][content=?]', 'csrf-token', @token
300 308 end
301 309 end
302 310
@@ -306,7 +314,7 @@ def generate_key(secret)
306 314 end
307 315 end
308 316
309   -class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
  317 +class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
310 318 def setup
311 319 @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
312 320 end
@@ -375,7 +383,7 @@ def teardown
375 383 end
376 384
377 385 def test_should_allow_custom_token
378   - post :index, :custom_token_name => 'foobar'
  386 + post :index, :custom_token_name => SecureRandom.base64(64)
379 387 assert_response :ok
380 388 end
381 389 end

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.