Issues (910)

framework/filters/Cors.php (3 issues)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\filters;
9
10
use Yii;
11
use yii\base\ActionFilter;
12
use yii\base\InvalidConfigException;
13
use yii\web\Request;
14
use yii\web\Response;
15
16
/**
17
 * Cors filter implements [Cross Origin Resource Sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing).
18
 *
19
 * Make sure to read carefully what CORS does and does not. CORS do not secure your API,
20
 * but allow the developer to grant access to third party code (ajax calls from external domain).
21
 *
22
 * You may use CORS filter by attaching it as a behavior to a controller or module, like the following,
23
 *
24
 * ```php
25
 * public function behaviors()
26
 * {
27
 *     return [
28
 *         'corsFilter' => [
29
 *             'class' => \yii\filters\Cors::class,
30
 *         ],
31
 *     ];
32
 * }
33
 * ```
34
 *
35
 * The CORS filter can be specialized to restrict parameters, like this,
36
 * [MDN CORS Information](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)
37
 *
38
 * ```php
39
 * public function behaviors()
40
 * {
41
 *     return [
42
 *         'corsFilter' => [
43
 *             'class' => \yii\filters\Cors::class,
44
 *             'cors' => [
45
 *                 // restrict access to
46
 *                 'Origin' => ['http://www.myserver.com', 'https://www.myserver.com'],
47
 *                 // Allow only POST and PUT methods
48
 *                 'Access-Control-Request-Method' => ['POST', 'PUT'],
49
 *                 // Allow only headers 'X-Wsse'
50
 *                 'Access-Control-Request-Headers' => ['X-Wsse'],
51
 *                 // Allow credentials (cookies, authorization headers, etc.) to be exposed to the browser
52
 *                 'Access-Control-Allow-Credentials' => true,
53
 *                 // Allow OPTIONS caching
54
 *                 'Access-Control-Max-Age' => 3600,
55
 *                 // Allow the X-Pagination-Current-Page header to be exposed to the browser.
56
 *                 'Access-Control-Expose-Headers' => ['X-Pagination-Current-Page'],
57
 *             ],
58
 *
59
 *         ],
60
 *     ];
61
 * }
62
 * ```
63
 *
64
 * For more information on how to add the CORS filter to a controller, see
65
 * the [Guide on REST controllers](guide:rest-controllers#cors).
66
 *
67
 * @author Philippe Gaultier <[email protected]>
68
 * @since 2.0
69
 */
70
class Cors extends ActionFilter
71
{
72
    /**
73
     * @var Request|null the current request. If not set, the `request` application component will be used.
74
     */
75
    public $request;
76
    /**
77
     * @var Response|null the response to be sent. If not set, the `response` application component will be used.
78
     */
79
    public $response;
80
    /**
81
     * @var array define specific CORS rules for specific actions
82
     */
83
    public $actions = [];
84
    /**
85
     * @var array Basic headers handled for the CORS requests.
86
     */
87
    public $cors = [
88
        'Origin' => ['*'],
89
        'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
90
        'Access-Control-Request-Headers' => ['*'],
91
        'Access-Control-Allow-Credentials' => null,
92
        'Access-Control-Max-Age' => 86400,
93
        'Access-Control-Expose-Headers' => [],
94
    ];
95
96
97
    /**
98
     * {@inheritdoc}
99
     */
100 3
    public function beforeAction($action)
101
    {
102 3
        $this->request = $this->request ?: Yii::$app->getRequest();
103 3
        $this->response = $this->response ?: Yii::$app->getResponse();
104
105 3
        $this->overrideDefaultSettings($action);
106
107 3
        $requestCorsHeaders = $this->extractHeaders();
108 3
        $responseCorsHeaders = $this->prepareHeaders($requestCorsHeaders);
109 3
        $this->addCorsHeaders($this->response, $responseCorsHeaders);
110
111 3
        if ($this->request->isOptions && $this->request->headers->has('Access-Control-Request-Method')) {
0 ignored issues
show
Bug Best Practice introduced by
The property headers does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property isOptions does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
112
            // it is CORS preflight request, respond with 200 OK without further processing
113 1
            $this->response->setStatusCode(200);
0 ignored issues
show
The method setStatusCode() does not exist on yii\console\Response. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

113
            $this->response->/** @scrutinizer ignore-call */ 
114
                             setStatusCode(200);
Loading history...
114 1
            return false;
115
        }
116
117 3
        return true;
118
    }
119
120
    /**
121
     * Override settings for specific action.
122
     * @param \yii\base\Action $action the action settings to override
123
     */
124 3
    public function overrideDefaultSettings($action)
125
    {
126 3
        if (isset($this->actions[$action->id])) {
127
            $actionParams = $this->actions[$action->id];
128
            $actionParamsKeys = array_keys($actionParams);
129
            foreach ($this->cors as $headerField => $headerValue) {
130
                if (in_array($headerField, $actionParamsKeys)) {
131
                    $this->cors[$headerField] = $actionParams[$headerField];
132
                }
133
            }
134
        }
135
    }
136
137
    /**
138
     * Extract CORS headers from the request.
139
     * @return array CORS headers to handle
140
     */
141 3
    public function extractHeaders()
142
    {
143 3
        $headers = [];
144 3
        foreach (array_keys($this->cors) as $headerField) {
145 3
            $serverField = $this->headerizeToPhp($headerField);
146 3
            $headerData = isset($_SERVER[$serverField]) ? $_SERVER[$serverField] : null;
147 3
            if ($headerData !== null) {
148 2
                $headers[$headerField] = $headerData;
149
            }
150
        }
151
152 3
        return $headers;
153
    }
154
155
    /**
156
     * For each CORS headers create the specific response.
157
     * @param array $requestHeaders CORS headers we have detected
158
     * @return array CORS headers ready to be sent
159
     */
160 3
    public function prepareHeaders($requestHeaders)
161
    {
162 3
        $responseHeaders = [];
163
        // handle Origin
164 3
        if (isset($requestHeaders['Origin'], $this->cors['Origin'])) {
165 2
            if (in_array($requestHeaders['Origin'], $this->cors['Origin'], true)) {
166
                $responseHeaders['Access-Control-Allow-Origin'] = $requestHeaders['Origin'];
167
            }
168
169 2
            if (in_array('*', $this->cors['Origin'], true)) {
170
                // Per CORS standard (https://fetch.spec.whatwg.org), wildcard origins shouldn't be used together with credentials
171 2
                if (isset($this->cors['Access-Control-Allow-Credentials']) && $this->cors['Access-Control-Allow-Credentials']) {
172
                    if (YII_DEBUG) {
173
                        throw new InvalidConfigException("Allowing credentials for wildcard origins is insecure. Please specify more restrictive origins or set 'credentials' to false in your CORS configuration.");
174
                    } else {
175
                        Yii::error("Allowing credentials for wildcard origins is insecure. Please specify more restrictive origins or set 'credentials' to false in your CORS configuration.", __METHOD__);
176
                    }
177
                } else {
178 2
                    $responseHeaders['Access-Control-Allow-Origin'] = '*';
179
                }
180
            }
181
        }
182
183 3
        $this->prepareAllowHeaders('Headers', $requestHeaders, $responseHeaders);
184
185 3
        if (isset($requestHeaders['Access-Control-Request-Method'])) {
186
            $responseHeaders['Access-Control-Allow-Methods'] = implode(', ', $this->cors['Access-Control-Request-Method']);
187
        }
188
189 3
        if (isset($this->cors['Access-Control-Allow-Credentials'])) {
190 1
            $responseHeaders['Access-Control-Allow-Credentials'] = $this->cors['Access-Control-Allow-Credentials'] ? 'true' : 'false';
191
        }
192
193 3
        if (isset($this->cors['Access-Control-Max-Age']) && $this->request->getIsOptions()) {
194 1
            $responseHeaders['Access-Control-Max-Age'] = $this->cors['Access-Control-Max-Age'];
195
        }
196
197 3
        if (isset($this->cors['Access-Control-Expose-Headers'])) {
198 1
            $responseHeaders['Access-Control-Expose-Headers'] = implode(', ', $this->cors['Access-Control-Expose-Headers']);
199
        }
200
201 3
        if (isset($this->cors['Access-Control-Allow-Headers'])) {
202 1
            $responseHeaders['Access-Control-Allow-Headers'] = implode(', ', $this->cors['Access-Control-Allow-Headers']);
203
        }
204
205 3
        return $responseHeaders;
206
    }
207
208
    /**
209
     * Handle classic CORS request to avoid duplicate code.
210
     * @param string $type the kind of headers we would handle
211
     * @param array $requestHeaders CORS headers request by client
212
     * @param array $responseHeaders CORS response headers sent to the client
213
     */
214 3
    protected function prepareAllowHeaders($type, $requestHeaders, &$responseHeaders)
215
    {
216 3
        $requestHeaderField = 'Access-Control-Request-' . $type;
217 3
        $responseHeaderField = 'Access-Control-Allow-' . $type;
218 3
        if (!isset($requestHeaders[$requestHeaderField], $this->cors[$requestHeaderField])) {
219 3
            return;
220
        }
221
        if (in_array('*', $this->cors[$requestHeaderField])) {
222
            $responseHeaders[$responseHeaderField] = $this->headerize($requestHeaders[$requestHeaderField]);
223
        } else {
224
            $requestedData = preg_split('/[\\s,]+/', $requestHeaders[$requestHeaderField], -1, PREG_SPLIT_NO_EMPTY);
225
            $acceptedData = array_uintersect($requestedData, $this->cors[$requestHeaderField], 'strcasecmp');
226
            if (!empty($acceptedData)) {
227
                $responseHeaders[$responseHeaderField] = implode(', ', $acceptedData);
228
            }
229
        }
230
    }
231
232
    /**
233
     * Adds the CORS headers to the response.
234
     * @param Response $response
235
     * @param array $headers CORS headers which have been computed
236
     */
237 3
    public function addCorsHeaders($response, $headers)
238
    {
239 3
        if (empty($headers) === false) {
240 3
            $responseHeaders = $response->getHeaders();
241 3
            foreach ($headers as $field => $value) {
242 3
                $responseHeaders->set($field, $value);
243
            }
244
        }
245
    }
246
247
    /**
248
     * Convert any string (including php headers with HTTP prefix) to header format.
249
     *
250
     * Example:
251
     *  - X-PINGOTHER -> X-Pingother
252
     *  - X_PINGOTHER -> X-Pingother
253
     * @param string $string string to convert
254
     * @return string the result in "header" format
255
     */
256
    protected function headerize($string)
257
    {
258
        $headers = preg_split('/[\\s,]+/', $string, -1, PREG_SPLIT_NO_EMPTY);
259
        $headers = array_map(function ($element) {
260
            return str_replace(' ', '-', ucwords(strtolower(str_replace(['_', '-'], [' ', ' '], $element))));
261
        }, $headers);
262
        return implode(', ', $headers);
263
    }
264
265
    /**
266
     * Convert any string (including php headers with HTTP prefix) to header format.
267
     *
268
     * Example:
269
     *  - X-Pingother -> HTTP_X_PINGOTHER
270
     *  - X PINGOTHER -> HTTP_X_PINGOTHER
271
     * @param string $string string to convert
272
     * @return string the result in "php $_SERVER header" format
273
     */
274 3
    protected function headerizeToPhp($string)
275
    {
276 3
        return 'HTTP_' . strtoupper(str_replace([' ', '-'], ['_', '_'], $string));
277
    }
278
}
279