Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

CSRF Protection Part 1

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We've gotta talk about one more thing: security. Specifically, CSRF attacks.

CSRF Attack?

Imagine if a malicious person built an HTML form on a totally different site, but set its action="" attribute to a URL on our site. Then, what if some user, like me, who is logged into our site, was tricked into submitting that form? Well, the form would submit, I would of course be authenticated, and the request would be successful! That's a problem! The malicious user was basically able to make a request to our site logged in as me! They could have done anything!

The other possible attack vector is if a malicious user runs JavaScript on their site that makes an AJAX call to our site. The result is exactly the same.

Do APIs Need Protection?

So, how can we protect against this in an API? The answer... you might not need to. If you follow two rules, then CSRF attacks are not possible.

Tip

Update: A more secure option is now available: to use SameSite cookies, which are now supported by most browsers and can be enabled in Symfony: https://symfony.com/blog/new-in-symfony-4-2-samesite-cookie-configuration. If you need to support older browsers, using CSRF tokens is best.

First, disallow AJAX requests from all domains except for your domain. Actually, this is just how the Internet works: you can't make AJAX requests across domains. If you do need to allow other domains to make AJAX requests to your domain, you do that by setting CORS headers. If you're in this situation, just make sure to only allow specific domains you trust, not everyone. This first rule prevents bad AJAX calls.

For the second rule, look at our API: src/Controller/RepLogController. Find newRepLogAction(). Notice that the body of the request is JSON. This is the second rule for CSRF protection: only allow data to be sent to your server as JSON. This protects us from, for example, bad forms that submit to our site. Forms cannot submit their data as JSON.

If you follow these two rules - which you probably do - then you do not need to worry about CSRF. But, to be fully sure, we are going to add one more layer: we're going to force all requests to our API to have a Content-Type header set to application/json. By requiring that, there is no way for a bad request to be made to our site, unless we're allowing it with our CORS headers.

Oh, and important side note: CSRF attacks only affect you if you allow session-based authentication like we're doing, or HTTP basic authentication. If you require an API token, you're also good!

Creating the Event Susbcriber

We're going to require the Content-Type header by creating an event subscriber, so that we don't need to add this code in every controller. First, to speed things up, install MakerBundle:

composer require "maker:^1.35" --dev

When that finishes, run:

php bin/console make:subscriber

Call it ApiCsrfValidationSubscriber. And, listen to the kernel.request event. Done! This made one change: it created a new class in src/EventSubscriber.

... lines 1 - 7
class ApiCsrfValidationSubscriber implements EventSubscriberInterface
{
public function onKernelRequest(GetResponseEvent $event)
{
// ...
}
public static function getSubscribedEvents()
{
return [
'kernel.request' => 'onKernelRequest',
];
}
}

Awesome! Because we're listening to kernel.request, the onKernelRequest() method will be called on every request, before the controller. At the top of the method, first say if !$event->isMasterRequest(), then return. That's an internal detail to make sure we only run this code for a real request.

... lines 1 - 9
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
... lines 15 - 21
}
... lines 23 - 31

Next, we do not need to require the Content-Type header for safe HTTP methods, like GET or HEAD, because, unless we do something awful in our code, these requests don't change anything on the server. Add $request = $event->getRequest(). Then, if $request->isMethodSafe(false), just return again.

... lines 1 - 15
$request = $event->getRequest();
// no validation needed on safe methods
if ($request->isMethodSafe(false)) {
return;
}
... lines 22 - 31

The false part isn't important: that's a flag for a backwards-compatibility layer.

Perfect! Next, we need to determine whether or not this request is to our api. We'll do that with a cool annotation trick. Then, we'll make sure the Content-Type header is set to application/json.

Leave a comment!

6
Login or Register to join the conversation
Mouad E. Avatar
Mouad E. Avatar Mouad E. | posted 3 years ago

Hi weaverryan am sending a post request from a react app to a sf4 api app, should i delete $event->isMasterRequest() because an option request comes before my post request and sf treated it as a sub request and return 405 error, what should i do?

Reply

Hey @adam!

Hmm.

> because an option request comes before my post request and sf treated it as a sub request

This should *not* be the case. Both requests - the POST and the HEAD - are *real* requests into your app and are both *master* requests. I'm 100% sure about that :). So if you're seeing that the HEAD is a sub-request, it must mean that while handling the HEAD "master" request, something else internally is making a sub-request. Basically, my point is: look a bit harder and what's going on... because something isn't right :).

Btw, that HEAD request is likely for CORS. Are you using something like NelmioCorsBundle to return the CORS headers? If so, *it* should be the thing that is responsible for returning the correct response (instead of allowing it to hit your controller, which will return a 405 if it's only set up to respond to POST requests).

Cheers!

Reply
Anthony R. Avatar
Anthony R. Avatar Anthony R. | posted 4 years ago

Hi Ryan,

I hope you’re excellent, it’s been a long time.

We need to be careful here because I feel the information mentioned here is misguiding:

> First, disallow AJAX requests from all domains except for your domain. Actually, this is just how the Internet works: you can't make AJAX requests across domains.

It seems you are referring to the same-origin policy. However SOP is designed to prevent websites from reading credentialed responses from a third party but not to prevent from posting data. It is possible to mutate state on another website via XHRs without any problem, SOP only makes it impossible to read the response. Yes, we can make cross domain XHR requests, we are just not allowed to read responses (depending on the CORS policy in place). It is also possible to make that XHR credentialed using ‘withCredentials: true’.
So here, with current tuto in place, we are subject to CSRF attacks.

> This is the second rule for CSRF protection: only allow data to be sent to your server as JSON.

The way data is represented is nothing to do with CSRF protection - using json does not protect users further. The risk here comes from the session-based authentication. An authenticated POST is an authenticated POST.. the way the data is represented is irrelevant for a CSRF defense.

Protecting SPA against CSRF and XSS attacks is not easy, but here I believe both points would misguide users in thinking they are protected when they would not be.

Just a heads up!

P.S. I think it would be helpful to a lot of people if you covered a safe login authentication for an SPA which would be immune to CSRF login attacks, this is not easy to do, whether one uses session based authentication OR token based authentication.

Reply

Hey Anthony R. !

Just an update on this. Someone else pointed out that these protections ay in fact *not* be enough as browsers have introeduced a feature (https://github.com/pillarjs... that indeed may allow application/json requests to be sent across JavaScript without preflight :/. And more broadly, browsers seem to think that relying on this is not sufficient. We're adding a note about this, and so, indeed, using CSRF tokens is best. We show how in this tutorial and there is also the wonderful bundle you linked to: https://github.com/dunglas/...

Cheers!

Reply

Hey Anthony R.!

Thanks for the post - seriously :). This was a tough chapter because it's security-related and I wanted to get it right. Now, I really want to make sure that I've got it right, with your help. So, I have a few questions:

However SOP is designed to prevent websites from reading credentialed responses from a third party but not to prevent from posting data. It is possible to mutate state on another website via XHRs without any problem

How? I mean, I know one way: via form posts, which could be on the bad domain with an action=http://thegoodomain.example or even with the good domain form hidden in an iframe on the bad domain. But, there is no other way to make a request via XHR, or JSONP (other than a GET request) to another domain, unless that domain has allowed it via CORS, right?

The way data is represented is nothing to do with CSRF protection - using json does not protect users further. The risk here comes from the session-based authentication. An authenticated POST is an authenticated POST.. the way the data is represented is irrelevant for a CSRF defense.

Actually, that's not entirely true :). As I mentioned above, there IS certainly an attack vector to make a POST request to another domain using a traditional HTML. But, HTML forms always submit with Content-Type: application/x-www-form-urlencoded or Content-Type: multipart/form-data. There is no way for the "bad" domain to add a form to their site and change this fact. So, if our API always checks for the Content-Type to be application/json (or really, anything other than the 2 that could be sent by a form), then it eliminates the CSRF attack from an embedded form.

Let me know what you think or what I'm not thinking about. From my perspective, the protection is a combination of (A) disallowing cross-domain via XHR... which only leaves GET requests and POST requests initiated by a form. And then (B) checking always for application/json Content-Type, which eliminates the form request. The only thing remaining are GET requests from other domains, which is safe because the attacker can't read the response (assuming, of course, that you haven't done something crazy and "mutated" your server on a GET request). Here's a source I was referencing for this section: https://github.com/pillarjs/understanding-csrf

Cheers!

Reply
Anthony R. Avatar

Hi Ryan,

I have now re-read your post as well as your answer.

> From my perspective, the protection is a combination of (A) disallowing cross-domain via XHR... which only leaves GET requests and POST requests initiated by a form. AND then (B) checking always for application/json Content-Type, which eliminates the form request.

Yes, yes and yes, not sure what was happening inside my head. I forgot preflight requests would fire on cross-origin POST request WHEN Content-Type is 'application/json' (more precisely when it is NOT one of application/x-www-form-urlencoded, multipart/form-data, or text/plain). I totally forgot about preflight... thank you!

So, yes, everything makes a lot of sense... apologies for the confusion and thank you so much for being so awesome.

Based on my very first point, I would still rephrase the following:

> First, disallow AJAX requests from all domains except for your domain. Actually, this is just how the Internet works: you can't make AJAX requests across domains.

into the following:

--> First, disallow AJAX requests from all domains except for your domain. Actually, this is just how the Internet works: you can't make AJAX requests across domains WHEN CONTENT-TYPE IS NOT ONE OF application/x-www-form-urlencoded, multipart/form-data, or text/plain.

Because, technically, it is possible to send cross-origin POST XHR requests with any of these 3 content-types and even if you can't read the response, you can mutate state. I.e. there is no preflight for these as they are Simple Requests:
https://developer.mozilla.o...

P.S. For those that would still would want to submit via `application/x-www-form-urlencoded`, I found the DunglasAngularCsrfBundle after some digging!
https://github.com/dunglas/...

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.9.1
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
        "doctrine/orm": "^2.5", // v2.7.2
        "friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
        "friendsofsymfony/user-bundle": "dev-master#4125505ba6eba82ddf944378a3d636081c06da0c", // dev-master
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.4
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/monolog-bundle": "^3.1", // v3.3.0
        "symfony/polyfill-apcu": "^1.0", // v1.9.0
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/swiftmailer-bundle": "^3.1", // v3.2.3
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/validator": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/twig": "2.10.*" // v2.10.0
    },
    "require-dev": {
        "symfony/debug-pack": "^1.0", // v1.0.6
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.5", // v1.5.0
        "symfony/phpunit-bridge": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0" // v4.1.4
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "dependencies": {
        "@babel/plugin-proposal-object-rest-spread": "^7.12.1" // 7.12.1
    },
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.5
        "@symfony/webpack-encore": "^0.26.0", // 0.26.0
        "babel-plugin-transform-object-rest-spread": "^6.26.0", // 6.26.0
        "babel-plugin-transform-react-remove-prop-types": "^0.4.13", // 0.4.13
        "bootstrap": "3", // 3.3.7
        "copy-webpack-plugin": "^4.4.1", // 4.5.1
        "core-js": "2", // 1.2.7
        "eslint": "^4.19.1", // 4.19.1
        "eslint-plugin-react": "^7.8.2", // 7.8.2
        "font-awesome": "4", // 4.7.0
        "jquery": "^3.3.1", // 3.3.1
        "promise-polyfill": "^8.0.0", // 8.0.0
        "prop-types": "^15.6.1", // 15.6.1
        "react": "^16.3.2", // 16.4.0
        "react-dom": "^16.3.2", // 16.4.0
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^7.0.0", // 7.3.1
        "sweetalert2": "^7.11.0", // 7.22.0
        "uuid": "^3.2.1", // 3.4.0
        "webpack-notifier": "^1.5.1", // 1.6.0
        "whatwg-fetch": "^2.0.4" // 2.0.4
    }
}
userVoice