• 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Strange behavior of CSRF tokens with a low csrf_expire (3.1.2)

#1
If you enable CSRF and a user sits on your login page for greater than the value of csrf_expire they will see a 403 forbidden page when they submit the form. Ideally CSRF should be transparent to a user so my goal is the following:

1. User idles on our login page for greater than csrf_expire
2. User submits the form over AJAX, error handler sees the 403 and makes another request white listed from CSRF checks to retrieve the latest token
3. Users initial request is resubmitted with the new token

config.php:

Code:
$config['cookie_prefix']    = '';
$config['cookie_path']        = '/';
$config['cookie_secure']    = TRUE;
$config['cookie_httponly']     = TRUE;

$config['csrf_protection'] = TRUE;
$config['csrf_token_name'] = 'csrf_token';
$config['csrf_cookie_name'] = 'csrf_cookie';
$config['csrf_expire'] = 10;
$config['csrf_regenerate'] = FALSE;
$config['csrf_exclude_uris'] = array('token');

I'm using 10 to make simulating the issue easier. Now things get weird. Look at the timestamps and CSRF values in this screenshot:

[Image: QODMrYT.png]

Under the first ~==~ the first login was submitted and the conduit captured the 403, made an AJAX call to /token to get the latest CSRF value, then sent the request again. That request also 403'd so once again it requested the CSRF token and received a different value. The original request was resubmitted and again 403'd. With csrf_expire set to 10, how can two different values exist <1s apart and neither be valid?

I know this is not a bug with how I'm handling CSRF tokens as I can set csrf_expire back to 7200 and enable csrf_regenerate and requests will correctly fail and request new tokens as you'd expect them when multiple requests are involved.

Has anyone else encountered this?

CI log set to 4:

Code:
INFO - 2017-01-26 16:31:32 --> Config Class Initialized
INFO - 2017-01-26 16:31:32 --> Hooks Class Initialized
DEBUG - 2017-01-26 16:31:32 --> UTF-8 Support Enabled
INFO - 2017-01-26 16:31:32 --> Utf8 Class Initialized
INFO - 2017-01-26 16:31:32 --> URI Class Initialized
INFO - 2017-01-26 16:31:32 --> Router Class Initialized
INFO - 2017-01-26 16:31:32 --> Output Class Initialized
INFO - 2017-01-26 16:31:32 --> Security Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Global POST, GET and COOKIE data sanitized
INFO - 2017-01-26 16:31:32 --> Config Class Initialized
INFO - 2017-01-26 16:31:32 --> Hooks Class Initialized
DEBUG - 2017-01-26 16:31:32 --> UTF-8 Support Enabled
INFO - 2017-01-26 16:31:32 --> Utf8 Class Initialized
INFO - 2017-01-26 16:31:32 --> URI Class Initialized
INFO - 2017-01-26 16:31:32 --> Router Class Initialized
INFO - 2017-01-26 16:31:32 --> Output Class Initialized
INFO - 2017-01-26 16:31:32 --> Security Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Global POST, GET and COOKIE data sanitized
INFO - 2017-01-26 16:31:32 --> Input Class Initialized
INFO - 2017-01-26 16:31:32 --> Language Class Initialized
INFO - 2017-01-26 16:31:32 --> Loader Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/app.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/app.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/storage.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/assets.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/metrics.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/defaults.php
INFO - 2017-01-26 16:31:32 --> Helper loaded: array_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: url_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: date_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: requirejs_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: dustjs_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: bootstrap_helper
INFO - 2017-01-26 16:31:32 --> Database Driver Class Initialized
INFO - 2017-01-26 16:31:32 --> Session: Class initialized using 'redis' driver.
INFO - 2017-01-26 16:31:32 --> Model Class Initialized
INFO - 2017-01-26 16:31:32 --> Controller Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Minifier shaved 0KB (0%) off final HTML output.
INFO - 2017-01-26 16:31:32 --> Final output sent to browser
DEBUG - 2017-01-26 16:31:32 --> Total execution time: 0.0456
INFO - 2017-01-26 16:31:32 --> Config Class Initialized
INFO - 2017-01-26 16:31:32 --> Hooks Class Initialized
DEBUG - 2017-01-26 16:31:32 --> UTF-8 Support Enabled
INFO - 2017-01-26 16:31:32 --> Utf8 Class Initialized
INFO - 2017-01-26 16:31:32 --> URI Class Initialized
INFO - 2017-01-26 16:31:32 --> Router Class Initialized
INFO - 2017-01-26 16:31:32 --> Output Class Initialized
INFO - 2017-01-26 16:31:32 --> Security Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Global POST, GET and COOKIE data sanitized
INFO - 2017-01-26 16:31:32 --> Config Class Initialized
INFO - 2017-01-26 16:31:32 --> Hooks Class Initialized
DEBUG - 2017-01-26 16:31:32 --> UTF-8 Support Enabled
INFO - 2017-01-26 16:31:32 --> Utf8 Class Initialized
INFO - 2017-01-26 16:31:32 --> URI Class Initialized
INFO - 2017-01-26 16:31:32 --> Router Class Initialized
INFO - 2017-01-26 16:31:32 --> Output Class Initialized
INFO - 2017-01-26 16:31:32 --> Security Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Global POST, GET and COOKIE data sanitized
INFO - 2017-01-26 16:31:32 --> Input Class Initialized
INFO - 2017-01-26 16:31:32 --> Language Class Initialized
INFO - 2017-01-26 16:31:32 --> Loader Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/app.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/app.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/storage.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/assets.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/metrics.php
DEBUG - 2017-01-26 16:31:32 --> Config file loaded: /Users/.../private/application/config/development/defaults.php
INFO - 2017-01-26 16:31:32 --> Helper loaded: array_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: url_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: date_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: requirejs_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: dustjs_helper
INFO - 2017-01-26 16:31:32 --> Helper loaded: bootstrap_helper
INFO - 2017-01-26 16:31:32 --> Database Driver Class Initialized
INFO - 2017-01-26 16:31:32 --> Session: Class initialized using 'redis' driver.
INFO - 2017-01-26 16:31:32 --> Model Class Initialized
INFO - 2017-01-26 16:31:32 --> Controller Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Minifier shaved 0KB (0%) off final HTML output.
INFO - 2017-01-26 16:31:32 --> Final output sent to browser
DEBUG - 2017-01-26 16:31:32 --> Total execution time: 0.0375
INFO - 2017-01-26 16:31:32 --> Config Class Initialized
INFO - 2017-01-26 16:31:32 --> Hooks Class Initialized
DEBUG - 2017-01-26 16:31:32 --> UTF-8 Support Enabled
INFO - 2017-01-26 16:31:32 --> Utf8 Class Initialized
INFO - 2017-01-26 16:31:32 --> URI Class Initialized
INFO - 2017-01-26 16:31:32 --> Router Class Initialized
INFO - 2017-01-26 16:31:32 --> Output Class Initialized
INFO - 2017-01-26 16:31:32 --> Security Class Initialized
DEBUG - 2017-01-26 16:31:32 --> Global POST, GET and COOKIE data sanitized
Reply

#2
IMO this whole thing is a bad idea, but I'd look at the Network tab and the Cookie header.
Reply

#3
What am I looking for? Here's screenshots of the cookie's for the request to /token and the 403 that happens immediately upon using said token.

/token:

[Image: 5QgflwF.png]

/login 403 with that token:

[Image: bMKVXm4.png]

The params tab shows the same CSRF value that was returned in the response object from the previous call to /token.

Found the issue. All the requests I was sending were POST. My browser cookie was not being updated because of this line:
https://github.com/bcit-ci/CodeIgniter/b...y.php#L209

Changing the /token request to GET and it all works now. Thanks!

Out of curiosity, you mentioned you thought this approach was a bad idea. Do you have an alternative solution where the user will never see a 403 error unless they are doing something they shouldn't be?
Reply

#4
(01-27-2017, 07:58 AM)spjonez Wrote: What am I looking for? Here's screenshots of the cookie's for the request to /token and the 403 that happens immediately upon using said token.

/token:

[Image: 5QgflwF.png]

/login 403 with that token:

[Image: bMKVXm4.png]

The params tab shows the same CSRF value that was returned in the response object from the previous call to /token.

These images don't show the csrf_cookie value - that's what you had to look for.

(01-27-2017, 07:58 AM)spjonez Wrote: Found the issue. All the requests I was sending were POST. My browser cookie was not being updated because of this line:
https://github.com/bcit-ci/CodeIgniter/b...y.php#L209

Changing the /token request to GET and it all works now. Thanks!

Well, no ... actually because of a bug that was fixed in 3.1.3, which also contains a lot of other security patches. You should've updated first.

(01-27-2017, 07:58 AM)spjonez Wrote: Out of curiosity, you mentioned you thought this approach was a bad idea. Do you have an alternative solution where the user will never see a 403 error unless they are doing something they shouldn't be?

Not just the approach, the entire idea is bad.

Knowingly submitting a request that you know will fail is obviously bad, but while tracking the token by expiry time would be more clean, it is still not a bullet-proof approach. In any case, whatever you do will amount to the same effect as setting a huge expiry time.

But the real problem is that you're going out of your way to disable a security feature. Often overlooked, but CSRF also prevents against replay attacks, and against someone physically impersonating careless users who leave their browsers open. What you're trying to do nullifies those protections and you'll now always refill the form with a valid token.
Reply

#5
(01-27-2017, 09:40 AM)Narf Wrote: Well, no ... actually because of a bug that was fixed in 3.1.3, which also contains a lot of other security patches. You should've updated first.

There's a few bugs that slipped through in that release, I'm waiting for 3.1.4 to see if it's stable.

(01-27-2017, 09:40 AM)Narf Wrote: Not just the approach, the entire idea is bad.

Knowingly submitting a request that you know will fail is obviously bad, but while tracking the token by expiry time would be more clean, it is still not a bullet-proof approach. In any case, whatever you do will amount to the same effect as setting a huge expiry time.

But the real problem is that you're going out of your way to disable a security feature. Often overlooked, but CSRF also prevents against replay attacks, and against someone physically impersonating careless users who leave their browsers open. What you're trying to do nullifies those protections and you'll now always refill the form with a valid token.

I don't follow. One of these two will happen;

1. User submits the form, sees a 403, clicks back and refreshes the page. User submits and it goes through.
2. User submits the form, 403 is handled, corrected, and the request goes through.

The first is a bad user experience. If they use the back button and not a link on the 403 page they have to refresh the page again or they will see another 403. They aren't going to understand the message, why they need a token, why leaving a page open causes this to occur, etc. In either case if the user is in front of the computer (being theirs or not) they will gain access.

It's not feasible to use csrf_regenerate in our application as we make a lot of concurrent requests. With this change we could, but it would not be performant and would result in a lot of duplicate calls to pull updated tokens.

The token is not stored in the form. It's saved as a property on the main app class (js) and passed to conduits each request. Every response includes the new token which is handed back to the app class for future use.

Our setup is a bit unique in that the only way to make a request is via AJAX post and the entire app is behind a login. Other than a few specific routes that allow get requests, but will not respond to system requests, it's the only way to talk to the app. CSRF is only one layer of protection we use we do not rely on this as the sole means of access.
Reply

#6
(01-27-2017, 10:02 AM)spjonez Wrote:
(01-27-2017, 09:40 AM)Narf Wrote: Well, no ... actually because of a bug that was fixed in 3.1.3, which also contains a lot of other security patches. You should've updated first.

There's a few bugs that slipped through in that release, I'm waiting for 3.1.4 to see if it's stable.

Cons: 2 know regressions, with known fixes that are easy to do even by hand.
Pros: Other bugfixes aside - 5 security patches, one of them fixing the bug that I told you about.

The choice should be obvious, but I hope you at least checked to see if you're affected by the vulnerabilities in question.

(01-27-2017, 10:02 AM)spjonez Wrote:
(01-27-2017, 09:40 AM)Narf Wrote: Not just the approach, the entire idea is bad.

Knowingly submitting a request that you know will fail is obviously bad, but while tracking the token by expiry time would be more clean, it is still not a bullet-proof approach. In any case, whatever you do will amount to the same effect as setting a huge expiry time.

But the real problem is that you're going out of your way to disable a security feature. Often overlooked, but CSRF also prevents against replay attacks, and against someone physically impersonating careless users who leave their browsers open. What you're trying to do nullifies those protections and you'll now always refill the form with a valid token.

I don't follow. One of these two will happen;

1. User submits the form, sees a 403, clicks back and refreshes the page. User submits and it goes through.
2. User submits the form, 403 is handled, corrected, and the request goes through.

1. User confirms that it is them that spent a long time on the page and indeed they are submitting it.
2. You assume what the user wants, and that it is the same user you are assisting.

(01-27-2017, 10:02 AM)spjonez Wrote: The first is a bad user experience. If they use the back button and not a link on the 403 page they have to refresh the page again or they will see another 403.

This is not uncommon. It may be mildly annoying, but not at all unreasonable ... not everything can magically work.
The CSRF token is a self-contained session; an authentication mechanism. Would you auto-create and authorize sessions?

(01-27-2017, 10:02 AM)spjonez Wrote: They aren't going to understand the message, why they need a token, why leaving a page open causes this to occur, etc.

They don't need to even read to word "token", let alone try to understand it. You can change the message to say they've spent too much time on the page before submitting and that's it.

Either way, security is more important than whether they understand the message or not.

(01-27-2017, 10:02 AM)spjonez Wrote: In either case if the user is in front of the computer (being theirs or not) they will gain access.

The problem isn't the legitimate user, but impersonators.

(01-27-2017, 10:02 AM)spjonez Wrote: The token is not stored in the form. It's saved as a property on the main app class (js) and passed to conduits each request. Every response includes the new token which is handed back to the app class for future use.

When talking about a form being submitted, you can safely assume I mean the payload being sent. Smile
Reply

#7
I don't believe the two regressions effect us, but we've had issues in the past few releases with the image library so we've started waiting a week or so before applying updates. Part of our application requires HTML submission so we use a third party library to filter input.

We've been pen tested twice and neither firm was able to break in. An HSTS policy, LB security policy, some headers, and a few tricks all come together to secure the application. We do auto create sessions but they are not authorized until credentials and CSRF are verified.

Our app is built using RequireJS and has a hook based boot system. It will only respond to requests that conform to our requirements and blocks all other requests. In the /token route for example, system booting is disabled server side and a single route outputs a JSON blob only if certain conditions exist. You can't put that route into your URL bar and receive a response.

I see your point about impersonation, but the only way to properly fix that is to use per request CSRF tokens which we'll have to agree to disagree on. The other security measures we have in place prevent attacks that could be used to steal the users token.

We're getting off topic, thanks for the replies it helped pin down what was going on.

Cheers,
Reply


Digg   Delicious   Reddit   Facebook   Twitter   StumbleUpon  


Users browsing this thread:
1 Guest(s)


  Theme © 2014 iAndrew  
Powered By MyBB, © 2002-2017 MyBB Group.