laravel / ideas

Issues board used for Laravel internals discussions.
938 stars 28 forks source link

CSRF Token error handling improvement #783

Closed DCzajkowski closed 6 years ago

DCzajkowski commented 7 years ago

What do you say, if we had different errors depending on a state of a CSRF token and depending on different env.

Currently:

environment no token invalid token
production "The page has expired [...]" "The page has expired [...]"
development "The page has expired [...]" "The page has expired [...]"

Proposed change:

environment no token invalid token
production "Whoops, something went wrong" "The page has expired [...]"
development TokenMismatchException("No CSRF token in the request") TokenMismatchException("Invalid token provided")
laurencei commented 7 years ago

I dont like the "whoops" for no token in production. From an end user perspective, it gives no indication of what went wrong. I'd prefer to keep it as showing expired.

m1guelpf commented 7 years ago

@DCzajkowski My vote goes on keeping the expired message on both scenarios on production, as it's more user friendly.

unstoppablecarl commented 7 years ago

I dont like the "whoops" for no token in production. From an end user perspective, it gives no indication of what went wrong. I'd prefer to keep it as showing expired.

If a missing token could only be caused by a programming error I would agree that a whoops would be appropriate. The case can easily be created by a user clearing cookies, session etc though.

I would very much like to see the option of having a token missing/token miss-match exception in development to make it more clear what the problem is.

joys014 commented 7 years ago

In my Exception\Handler.php i have this in the render() function

if ($exception instanceof \Illuminate\Session\TokenMismatchException) { return redirect()->back()->with('message', 'Security token expired. Please retry.'); }

Works like a charm. U can add env('APP_ENV') == 'production' to further customize.

deleugpn commented 6 years ago

@unstoppablecarl

sisve commented 6 years ago

Wouldn't it make it easier to just drop CSRF checks for requests originating from the same host? All issues I've found affecting real humans has been people that has been really slow on filling in forms, leaving them up for ages before submitting them. And to be fair, there's no hint at all about any time limits to fill out that login form.

The first half of CSRF is the Cross-Site part, is there any known issue at all if we just don't validate the token when the referral host is the same as the current host (=same site)? I presume that no modern browser allows faking the referral. Browser that drops the referral due to privacy reasons will still see the normal CSRF checks.

Can an external page ever pass CSRF checks? Is there really such a thing as an "expired page" in that scenario?

slashequip commented 6 years ago

I presume it would be fairly easy to spoof the host which is why these checks exist in the first place? ¯\_(ツ)_/¯

deleugpn commented 6 years ago

@sisve How can you ever be sure? The whole HTTP Header is fakeable. Yes, browsers will implement no cross-origin policy, but nothing prevents me from pulling curl and calling the action portion of your form pretending to be your own host by faking the headers.

Am I wrong here?

laurencei commented 6 years ago

I presume that no modern browser allows faking the referral.

I have government clients still running IE6 & IE8 😭

And to be fair, there's no hint at all about any time limits to fill out that login form.

The Login Form is probably the one place where it is safe to disable your CSRF check (and put it in the excluded paths). Because there is nothing to CSRF if your not logged in.

But everything else needs protection.

sisve commented 6 years ago

Assuming a normal modern browser without any evil spoofing addons; no, there is no way to spoof the referrer or the host header. Anything that allows that is a security bug in the browser. (Or addons that you have to install yourself first.)

Sending custom requests in your favorite http client tool (postman, curl, wget, telnet, whistling into a phone booth) on the other hand allows you to set the headers to any value you want. But that's not really what CSRF is about.

CSRF is about your page validating who generated the form that is being posted (kind of). Imagine your online bank with a big button saying "transfer the funds to hacker". You are logged into the bank at all times because you like to look at the button. It's a really fancy button. CSRF protects the user/bank from the Evil Site posting requests with their own <form action="https://bank.com"> because the site does not have the CSRF token, and there is no way that the Evil Site can get the token. The token is bound to the session of the user, so the Evil Site cannot just issue a guzzle-request server-side to get a csrf token, because it wouldn't be the correct one.

So, if Evil Site decides to do a curl/guzzle request and fake referral headers, then sure, the request would not fail the csrf checks. It would however not be logged in as the user, with a big button ready to transfer the funds. It would be seen, from the bank's perspective, as just a new anonymous session from some new guy with a really suspect hostname.

Basically, CSRF protects against "rogue" requests within the same browser. The browser will issue the form post with the known cookies for the domain (read: current session of the user) and the Bank will see the request with an evil referrer, but correct data to perform the command... except that the csrf token is invalid, so it's an error.

CSRF does not protect evilness of people doing custom requests, or evil people that performs entire user flows (login, go to page, submit button) in Dusk/phantomjs/selenium.

... and this entire post probably fails when you need to support IE6. I have no knowledge of any exploits in IE6 that would allow you to fake the referrals for requests, but on the other hand I have purged my mind of IE6 a long time ago...

laurencei commented 6 years ago

At the end of the day OWASP recommend CSRF checks the way they are done now.

If Laravel didnt implement them in the "standard" way - it might create compliance issues for Enterprise clients.

I like your idea - but I think it's probably a step too far. Perhaps it could be a config option.

laurencei commented 6 years ago

Also:

According to RFC 6454 - The Web Origin Concept - the presence of Origin is actually legal for any HTTP request, including same-origin requests: "The user agent MAY include an Origin header field in any HTTP request."

So the RFC says there is no garuntee the origin header will even be there - it is a "MAY" not a "MUST".

This SO answer from 2013 says Firefox doesnt even include origin headers on same origin requests. Not sure if it is still correct in 2017 though: https://stackoverflow.com/a/15514049/1317935

sisve commented 6 years ago

I've talked about the Referer header and the Host header so far, not the Origin header.

Also, at the bottom of the OWASP article on CSRF, under "Related Controls":

Checking the referrer header in the client's HTTP request can prevent CSRF attacks. Ensuring that the HTTP request has come from the original site means that attacks from other sites will not function. It is very common to see referrer header checks used on embedded network hardware due to memory limitations.

laurencei commented 6 years ago

@sisve - you might be onto something... worth looking into more 👍

drbyte commented 6 years ago

It would be fantastic if the Referer header were sufficient to rely upon, but the reality is it can be spoofed. I can't count the hundreds of people I know whom I've had to tell them to remove one or more "browser plugins" that were doing rogue things they weren't aware of, and were hijacking their normal browsing activities ... including spoofing referer headers so that data could be stolen, sessions could be hijacked, and more.

(One way: use a strategically malformed link in an email to get the user to open the targeted site in their browser, auto-login due to established remembered session cookies, but then send that session data to a rogue server (crafted into the malformed link/javascript) and then the malicious user spoofs their headers to allow them to impersonate the tricked user. Same is done all over the web with rogue browser plugins.)

Given that it's impossible to ensure that even the honest innocent unwitting user doesn't have something unauthorized or malicious going on, one must unfortunately still assume the worst. Can't rely on referer header; other controls need to be present.

Principle: Don't Trust User Input. OWASP article says:

A user or client will not always submit data your application will expect. By building robust applications that do not trust user input by default, you ensure the application will be able to handle unexpected data gracefully. Examples of user input include: form data, client information such as user-agent strings, cookies, referer, etc. Anything that is submitted in an HTTP request should be considered user input.

sisve commented 6 years ago

But do we really care about the case when the attack has the capability to trick people into first installing browser addons? Couldn't they just do the evil things from within the addon to begin with, instead of using the addon as a stepping stone for a site to use?

How would any implementation of our stuff help at all if that poor helpless user has been taken over by the evil TeamViewer and someone else is controlling the cursor? (Or in my case, where broken trackpad causes it to go haywire and click randomly all over the screen...)

I believe you're misinterpreting the Don't Trust User Input rules in this context, and they are perhaps applicable as "don't count on the headers to contain ascii characters" or similar. Not that we cannot trust the values within these two headers (Referer, Host).

Other techniques

  • Checking the HTTP Referer header to see if the request is coming from an authorized page is commonly used for embedded network devices because it does not increase memory requirements. However, a request that omits the Referer header must be treated as unauthorized because an attacker can suppress the Referer header by issuing requests from FTP or HTTPS URLs. This strict Referer validation may cause issues with browsers or proxies that omit the Referer header for privacy reasons. Also, old versions of Flash (before 9.0.18) allow malicious Flash to generate GET or POST requests with arbitrary HTTP request headers using CRLF Injection.[29] Similar CRLF injection vulnerabilities in a client can be used to spoof the referrer of an HTTP request.

From: https://en.wikipedia.org/wiki/Cross-site_request_forgery

The Referer header field allows servers to generate back-links to other resources for simple analytics, logging, optimized caching, etc. It also allows obsolete or mistyped links to be found for maintenance. Some servers use the Referer header field as a means of denying links from other sites (so-called "deep linking") or restricting cross-site request forgery (CSRF), but not all requests contain it. [...] Some intermediaries have been known to indiscriminately remove Referer header fields from outgoing requests. This has the unfortunate side effect of interfering with protection against CSRF attacks, which can be far more harmful to their users.

Source: RFC 7231 (HTTP 1.1, Semantics and Content), section 5.5.2: Referer

So far I've linked both to OWASP's article on CSRF, a RFC on HTTP 1.1, and Wikipedia, both mentioning that the referer header can be used for CSRF validation. And as I've mentioned, the current CSRF token will still need to exist to handle the cases when the referer check fails (external referrer or just a missing header)

Every example of a spoofed referer header involves either pre-installed addons or security bugs in the browser. I think that those cases are borked to begin with; if I get to install an addon I will just silently inject a new <input type="hidden" name="account_target" value="sisve's wallet"> in your bank's payment page...

Does anyone know of any browser that allows you to spoof the referer header via javascript code, without first installing extra addons? (I added the requirement for javascript since it's the evil site that needs to do the spoofing, not the user enabling a feature manually in their browser. I would accept vbscript answers too.)

drbyte commented 6 years ago

Does anyone know of any browser that allows you to spoof the referer header via javascript code, without first installing extra addons?

Good question.

(Granted, I'm assuming you're ruling out curl and other completely tamperable tools like Postman, etc.)

taylorotwell commented 6 years ago

Open to improvements here if people want to contribute.

sisve commented 6 years ago

Well, if we want people to contribute and find available ideas, is it really appropriate to close the issue and hide it from them? Do we expect people to go searching in the list of closed issues to find ideas that they are welcome to build?

garygreen commented 5 years ago

@sisve

Interested in hearing more of your ideas and how you've managed to gracefully solve CSRF problems on your sites.

We're currently having issues where we're getting a lot of TokenMismatch/CSRF exceptions, it's very intermittent when it happens but it's happening often enough for us to want to explore other ways of handling cross site scripting protection.

I think one of the main reasons we're getting the issue is because we have infinite scrollers on our site, and on mobile Chrome caches the browser state so when you go back (after a few hours past the session "lifetime") and they attempt to scroll - it'll give a timeout error. Granted the AJAX request probably shouldn't be a POST, and we could add an exception to the page, but it's occuring on other pages as well that should legitimately use a POST.

I read an article saying that you can add protection against CSRF attacks just by using lax/strict cookie settings - do you have any thoughts on that?

At the moment to gracefully handle token issues - instead of showing the standard Laravel timeout page, instead we're redirecting back, generating a new session and setting the new token on the form with a message that says "Your sessions has timed out, please retry your request below" - this seems more user friendly than just showing a page that has a button on it to take you home.

But ideally would like to avoid these token timeouts altogether. It's just not very user friendly at all and creates a poor experience for users.

sisve commented 5 years ago

I believe I'm hit with a similar use-case. We believe our users are loading the login page, realizing that they are hungry, and go on a two-hour lunch before getting back and posting the form.

Our solution was to ignore skip the check if the user was from our own site. We use the Referer header for this, believing that no semi-modern browser allows you to fake this header in normal csrf scenarios.

My definition of a normal csrf scenario;

My code of the VerifyCsrfToken is based on the 5.5 LTS version of the middleware, thus the perhaps slightly different naming of methods than the current version. I believe the logic is still easy to read; if the incoming request is to the same host as the referer comes from, then we allow the request.

use Illuminate\Http\Request;

class VerifyCsrfToken extends \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken {

    public function handle($request, Closure $next) {
        if ($this->refererIsSameHost($request)) {
            return $this->addCookieToResponse($request, $next($request));
        }

        // TODO: the usual stuff
    }

    protected function refererIsSameHost(Request $request): bool {
        $referer = $request->headers->get('referer');
        if ($referer === null) {
            return false;
        }

        $refererHost = parse_url($referer, PHP_URL_HOST);
        return $refererHost === $request->getHost();
    }

}
garygreen commented 5 years ago

Interesting. I'm guessing the refererer solution works well for you in apps? I believe Twitter and other sites stop working when browsers don't send referrer header, so most users would always allow it to be sent in their browsers - though I'm not sure I would rely on it personally. Some proxies, VPNs and other services strip out referrers, including privacy-focused browsers.

I'm considering using the Origin header instead, which is supported by all browsers (including IE) and it cannot be spoofed by user's or removed. It's basically like referrer minus all the downsides.

On top of that, if there is no origin header, just fallback onto the standard cumbersome and fragile csrf tokens. In theory, 99% of users ever get timeouts with this method. What do you think?

sisve commented 5 years ago

Sure, there are several scenarios when there's no Referer header available, but those are usually special-cases. With my implementation the code falls back to the usual behavior when this happens and will verify the csrf token.

While the Origin header may be supported everywhere ... are you sure it's related to CSRF? I've always used it for CORS, but never for CSRF. When I am posting a form on my own page in IE11 I get a Referer header, but no Origin. Same thing if I modify my local form to post to an external site, there's a Referer header, but no Origin header.

garygreen commented 5 years ago

Your right, I assumed IE did add the Origin header as according to mdn it's supported, how strange.

image

According to OWASP they recommend to check Origin and then fallback onto refererr:

If the Origin header is not present, verify the hostname in the Referer header matches the target origin

https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md

garygreen commented 5 years ago

FYI - this is what I'm currently using. It uses origin, then refererr and if not falls back onto token based check. This should catch most csrf attacks and let thru legit requests minimizing amount of token exceptions when sessions expire etc.

Based off sisve versions above, but more tightly integrates with Laravel, allowing thru reading requests, console, testing, etc.

<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;

class VerifyCsrfToken extends BaseVerifier
{
    protected function tokensMatch($request)
    {
        return $this->originIsSameHost($request) ||
               $this->refererIsSameHost($request) ||
               parent::tokensMatch($request);
    }

    protected function originIsSameHost(Request $request): bool
    {
        return $request->headers->get('origin') == url('/');
    }

    protected function refererIsSameHost(Request $request): bool
    {
        return parse_url($request->headers->get('referer', ''), PHP_URL_HOST) == $request->getHost();
    }
}