yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.24k stars 6.91k forks source link

Regression: Cors::beforeAction() #15665

Closed alexraputa closed 6 years ago

alexraputa commented 6 years ago

There's a regression in 2.0.14-dev Cors::beforeAction() after merge: https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d

This is my test in Codeception:

public function preflight(ApiTester $I): void
{
    $I->haveHttpHeader('Access-Control-Request-Headers', 'Content-Type');
    $I->haveHttpHeader('Access-Control-Request-Method', 'POST');
    $I->haveHttpHeader('Accept', '*/*');
    $I->sendOPTIONS('/users');

    $I->seeResponseCodeIs(200);
    $I->seeHttpHeader('Content-Type', 'application/vnd.api+json; charset=UTF-8');
    $I->seeHttpHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
    $I->seeHttpHeader('Access-Control-Allow-Headers', 'Content-Type');
}

Before merge https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d:

See http header "Content-Type","application/vnd.api+json; charset=UTF-8"

- Expected | + Actual
@@ @@
-'application/vnd.api+json; charset=UTF-8'
+'application/vnd.api+json; charset=UTF-8'

After merge https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d:

See http header "Content-Type","application/vnd.api+json; charset=UTF-8"

- Expected | + Actual
@@ @@
-'application/vnd.api+json; charset=UTF-8'
+'text/html; charset=UTF-8'

Additional info

Q A
Yii version 2.0.14-dev
PHP version 7.1.4
Operating system macOS High Sierra 10.13.3
samdark commented 6 years ago

What's the actual code that is tested?

alexraputa commented 6 years ago

@samdark, this is an action that I testing:

public function actionCreate()
{
    $resource = new SignUpResource();

    if ($resource->load($this->request->post()) && $resource->validate()) {
        try {
            $this->service->create($resource);
            $this->response->setStatusCode(201);
            $this->response->getHeaders()->set('Location', Url::to('users/' . $resource->id, true));

            return $resource;
        } catch (\DomainException $e) {
            \Yii::$app->errorHandler->logException($e);
        }
    } elseif ($resource->hasErrors()) {
        return $resource;
    }
}
alexraputa commented 6 years ago

rest controller:

class BaseController extends \yii\web\Controller
{
...
    public $serializer = \tuyakhov\jsonapi\Serializer::class;

    public $enableCsrfValidation = false;

    public function behaviors(): array
    {
        return [
            'contentNegotiator' => [
                'class' => \yii\filters\ContentNegotiator::class,
                'formats' => [
                    'application/vnd.api+json' => \yii\web\Response::FORMAT_JSON,
                    'application/json' => \yii\web\Response::FORMAT_JSON,
                    'application/xml' => \yii\web\Response::FORMAT_XML,
                ],
            ],
            'verbFilter' => [
                'class' => \yii\filters\VerbFilter::class,
                'actions' => $this->verbs(),
            ],
            'rateLimiter' => [
                'class' => \yii\filters\RateLimiter::class,
            ],
        ];
    }

config:

'components' => [
    'request' => [
        'parsers' => [
            'application/vnd.api+json' => [
                'class' => tuyakhov\jsonapi\JsonApiParser::class,
                'memberNameCallback' => [tuyakhov\jsonapi\Inflector::class, 'variablize'],
            ],
            'application/json' => yii\web\JsonParser::class,
        ],
    ],
    'response' => [
        'formatters' => [
            yii\web\Response::FORMAT_JSON => [
                'class' => tuyakhov\jsonapi\JsonApiResponseFormatter::class,
                'prettyPrint' => YII_DEBUG,
                'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
            ],
        ],
    ],
    'urlManager' => [
        'enablePrettyUrl' => true,
        'enableStrictParsing' => true,
        'showScriptName' => false,
        'rules' => [
            ...
            'OPTIONS,GET users' => 'users/index',
            'OPTIONS,POST users' => 'users/create',
        ],
    ],
],
'as corsFilter' => [
    'class' => yii\filters\Cors::class,
],
samdark commented 6 years ago

It's not clear how to reproduce it. There are custom classes everywhere and excessive config. Would you please do a minimal test case that contains only necessary code to reproduce the issue starting with basic app?

alexraputa commented 6 years ago

@samdark, I'll try.

alexraputa commented 6 years ago

@samdark, the issue is reproduce in the basic app cors-bug.zip.

How to reproduce the issue.

  1. composer install
  2. php vendor/bin/codecept run api The result should be fail.
  3. These lines should be comment out. https://github.com/yiisoft/yii2/blob/4f80cda7130da5259ce93fe41eb681cbb70f30be/framework/filters/Cors.php#L109-L113 The result should be success.
leandrogehlen commented 6 years ago

This is strange bacause Cors filter is not enabled in this project

samdark commented 6 years ago

@leandrogehlen and you confirm that commenting lines above make tests pass?

leandrogehlen commented 6 years ago

I'm looking for it now

alexraputa commented 6 years ago

w/ https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d:

2018-02-19 19 43 27

w/o https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d:

2018-02-19 19 44 34
leandrogehlen commented 6 years ago

I did some tests and the unique issue is that if you comment second line the test works. Then, i can't see the problem, because the header: 'Content-Type' is not set in request and ContentNegotiator not add this header in response. I think this is right.


   $I->seeResponseCodeIs(200);
   //$I->seeHttpHeader('Content-Type', 'application/json; charset=UTF-8');
   $I->seeHttpHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
   $I->seeHttpHeader('Access-Control-Allow-Headers', 'Content-Type');
leandrogehlen commented 6 years ago

I think i found the problem, in example project was configured in web.php:

   'as corsFilter' => [
        'class' => yii\filters\Cors::class,
    ],
   'as authenticator' => [
        'class' => yii\filters\auth\CompositeAuth::class,
        'except' => ['site/index', 'debug/*'],
        'authMethods' => [
            ['class' => yii\filters\auth\HttpBearerAuth::class],
        ],
    ],

This way behavior method is not considered. I removed this configuration and did it in controller :

class SiteController extends Controller
{
    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            'contentNegotiator' => [
                'class' => ContentNegotiator::class,
                'formats' => [
                    'application/json' => Response::FORMAT_JSON,
                ],
            ],
            'cors' => [
                'class' => Cors::class,
            ]
        ]
   ...

I change the test to

    $I->sendOPTIONS('/site/index?_format=json');

This worked, but i think the problem is the sequence, like cors return false the following filters are not executed

samdark commented 6 years ago

Well, that's expected and exactly that was changed, isn't it?

alexraputa commented 6 years ago

This worked, but i think the problem is the sequence, like cors return false the following filters are not executed

This is the correct statement.

leandrogehlen commented 6 years ago

@samdark, i think so. @alexraputa if you change the filters sequence your problem is solved?

alexraputa commented 6 years ago

@leandrogehlen, no. If I change the filters sequence my authorization is failing. But the preflight test is passed.

leandrogehlen commented 6 years ago

@alexraputa why do you need these headers or authentication in preflight?

alexraputa commented 6 years ago

@leandrogehlen, this is definitely a bug. The preflight response should contain Content-Type, which will be the same as for a real response. See CORS the section of Preflighted requests and example for it.

leandrogehlen commented 6 years ago

I agreee, but this is happening. You don't adds the 'Content-Type' in your request test.

If you use the code bellow, will work

$I->haveHttpHeader('Content-Type', 'application/json; charset=UTF-8');
$I->haveHttpHeader('Access-Control-Request-Headers', 'Content-Type');
$I->haveHttpHeader('Access-Control-Request-Method', 'POST');
$I->haveHttpHeader('Accept', '*/*');
$I->sendOPTIONS('/users');
alexraputa commented 6 years ago

@leandrogehlen, I disagree with it. The Content-Type of the request specifies the request body, which is not applicable to an OPTIONS request. See OPTIONS request.

I use behaviors in this sequence. See Yii 2 CORS.

2018-02-20 19 38 05

The behaviors will not work if the first of them return false in the beforeAction method.

@samdark, I think we should revert this https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature, until we find a better solution.

leandrogehlen commented 6 years ago

@alexraputa, I understand but i belive the interruption in filter sequence is another issue, not associate to Cors. If it will considered a bug is a problem for all filters

leandrogehlen commented 6 years ago

@samdark, I can't see problem, does not make sense execute others filters in preflight, because after preflight the real request will be executed

alexraputa commented 6 years ago

@leandrogehlen, @samdark, Such frameworks as Laravel and Symfony does not stop the application in the cors filter if the request has OPTIONS method. They simply modify the response object and pass it further along the chain of middlewares. Why not do the same in Yii? We should not leave this as is.

leandrogehlen commented 6 years ago

@alexraputa, isn't to OPTIONS method is only in preflight: https://github.com/yiisoft/yii2/blob/master/framework/filters/Cors.php#L109

leandrogehlen commented 6 years ago

related #14618

alexraputa commented 6 years ago

@leandrogehlen, The preflight request is equal to OPTIONS request.

Unlike “simple requests”, "preflighted" requests first send an HTTP request by the OPTIONS method to the resource on the other domain, in order to determine whether the actual request is safe to send. Cross-site requests are preflighted like this since they may have implications to user data.

Example of a request from documentation (Preflighted requests):

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

Example of a response from documentation (Preflighted requests):

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

Example of a real request from documentation (Preflighted requests):

POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache

<?xml version="1.0"?><person><name>Arun</name></person>

Example of a real response from documentation (Preflighted requests):

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]

Both responses have the same content type - Content-Type: text/plain.

@samdark, need your thoughts.

alexraputa commented 6 years ago

This https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature also breaks BC if you have actionOptions like this:

public function actionOptions(): void
{
    if (!$this->request->getIsOptions()) {
        $this->response->setStatusCode(405);
    }

    $options = implode(', ', array_reduce($this->verbs(), 'array_merge', []));
    $this->response->getHeaders()->set('Allow', $options);
    $this->response->getHeaders()->set('Access-Control-Allow-Method', $options);
}

Because the response was stopped in the beforeAction() method of the CORS filter. And your response will not contain the Allow and the modified Access-Control-Allow-Method headers for specific actions of controllers.

samdark commented 6 years ago

@alexraputa according to CORS spec https://www.w3.org/TR/cors/#preflight-request the only considered headers are CORS-related. The rest of the headers and body are simply ignored so it doesn't make any sense to send them.

samdark commented 6 years ago

Why would you need to send Allow or Content-type if they're simply ignored?

alexraputa commented 6 years ago

Why would you need to send Allow or Content-type if they're simply ignored?

@samdark, This is not true. The response from the server must support these headers. See rfc7231 and OPTIONS W/ this https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature. My application on Ember.js 3.0 sends a preflight request:

request

And backend app on Yii 2 sends response:

response

W/o this https://github.com/yiisoft/yii2/commit/399dbce0cadbd7631f726841ece3ecf6a830444d feature.

response2

This is a standard preflight request to the server. The other frontend frameworks such as React and Angular send the same preflight request. If frontend app send the Accept: */* (see Accept) header then backend app must send the allowed Content-Type for a URL.

samdark commented 6 years ago

Well, this is not how preflight should work according to its specification: https://www.w3.org/TR/cors/#preflight-request

Do you have any actual misbehavior caused in your app because of the change?

alexraputa commented 6 years ago

My API is public and clients are using it. If the Accept header receives the response with Content-Type liketext/html, then clients accordingly need to implement a serializer for this type of content. But then the API sends a real response with the type application/vnd.api+json and the frontend application throws an error, because the serializer for application/vnd.api+json is not implemented.

leandrogehlen commented 6 years ago

@alexraputa, this is not true, i have built an angular application with yii2 cors filters and it worked fine.

Preflight results: preflight

Real request: real-request

Yii2 Angular Project yii2-angular.zip

cd angular
npm install
ng build --watch

to access angular app use: http://localhost/yii2-angular/web/dist/

alexraputa commented 6 years ago

@leandrogehlen, I don't see any of CORS methods in your response. The preflight request shouldn't have the Content-Type header because it has an empty body. But you force added the Content-Type header into the request.

let headers = new HttpHeaders({
  'Content-Type': 'application/json'
});
leandrogehlen commented 6 years ago

@samdark, please close this issue, he wants to have a response default format. it has nothing to do with issue.

alexraputa commented 6 years ago

Ok, if you want to have some rare bugs in a framework then just leave it.

samdark commented 6 years ago

@alexraputa which client do you have that doesn't follow CORS specification and looks at non-CORS headers in preflight?

alexraputa commented 6 years ago

@samdark, Which the headers of the my preflight request do not match with the CORS specification?

samdark commented 6 years ago

Not the CORS specification itself but preflight part of it will just ignore what's not needed for CORS:

What matters (and is cached in browsers) in CORS reponse is:

That's it. Nothing about content-type, accept or anything else. Body doesn't matter as well.

So I'm asking again, what's the client that you're using that needs Accept header in the response for preflight request?

alexraputa commented 6 years ago

@samdark,

My application on Ember.js 3.0 sends a preflight request:

These are preflight requests specification in ASP.NET (Preflight-requests):

  • The application doesn't set any request headers other than Accept, Accept-Language, Content-Language, Content-Type, or Last-Event-ID, and

  • The Content-Type header (if set) is one of the following: application/x-www-form-urlencoded multipart/form-data text/plain

You can close this issue If you think that this is not a bug.

samdark commented 6 years ago

You're confusing request and response. See response example there:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 0
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT
Date: Wed, 20 May 2015 06:33:22 GMT

According to W3C spec, Cache-Control: no-cache, Pragma: no-cache and Date are ignored and aren't cached on clients so it comes to:

HTTP/1.1 200 OK
Content-Length: 0
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT

What exactly happened to Ember.js 3.0 app when we started to send these reduced CORS responses? Did it break somehow? How exactly?

tunecino commented 6 years ago

These are preflight requests specification in ASP.NET (Preflight-requests)

@alexraputa according to that linked documentation those are not specifications but conditions that when meet the browser will omit or skip the process of sending a preflight request.

samdark commented 6 years ago

Those are conditions similar to what's on W3C except examples are a bit weird. They're OK but, as I said, they define request, not response.

alexraputa commented 6 years ago

@samdark, what do you think about it Preflighted requests (see example of preflight request/response)? And about this https://github.com/yiisoft/yii2/issues/15665#issuecomment-367489876. We already have the actionOptions method in the ActiveController class. The headers of this action are never included in the response if you use the CORS filter.

https://github.com/yiisoft/yii2/blob/77ad6bc00847d4964a0b2a82d3b70dcd7cb5a1cf/framework/rest/ActiveController.php#L102-L104 https://github.com/yiisoft/yii2/blob/77ad6bc00847d4964a0b2a82d3b70dcd7cb5a1cf/framework/rest/OptionsAction.php#L32-L44

What exactly happened to Ember.js 3.0 app when we started to send these reduced CORS responses? Did it break somehow? How exactly?

The front-end application is working. But the response from the API was significantly reduced, which does not give a full submission of the resource.

samdark commented 6 years ago

I think that these response examples are OK as well but, as pointed out by specification (W3C one), non-CORS headers are not taken into account. Means you can send whatever you want: any headers, body... anything. As long as response code is 2xx and CORS headers are there, it's fine. But these non-CSRF headers doesn't matter.

samdark commented 6 years ago

About https://github.com/yiisoft/yii2/issues/15665#issuecomment-367489876. Regular OPTIONS request and preflight request are two different things. There's no need to give out usual options response in CORS preflight response.

Or did you mean that regular OPTIONS response (non-preflight one) is broken?

samdark commented 6 years ago

The front-end application is working. But the response from the API was significantly reduced, which does not give a full submission of the resource.

Would you please elaborate "does not give a full submission of the resource"?

alexraputa commented 6 years ago

@samdark, https://github.com/yiisoft/yii2/issues/15665#issuecomment-367602078.

You can close this issue If you think that this is not a bug.

samdark commented 6 years ago

I don't get it :( Does this change affect non-preflight responses?

alexraputa commented 6 years ago

No, it's not affect. But it's confusing.

I don't modify any response on API, but framework send the Content-Type: text/html.

preflight-requests preflight-request