Skip to content

Commit d61eb96

Browse files
Merge pull request christoph-kluge#8 from christoph-kluge/custom-origin
Feature: Add ability to enable strict host checking
2 parents bdd79c3 + 687ac18 commit d61eb96

File tree

7 files changed

+110
-36
lines changed

7 files changed

+110
-36
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The defaults for this middleware are mainly taken from [enable-cors.org](https:/
3636

3737
Thanks to [expressjs/cors#configuring-cors](https://github.com/expressjs/cors#configuring-cors). As I took most configuration descriptions from there.
3838

39+
* `server_url`: can be used to set enable strict `Host` header checks to avoid malicious use of our server. (default: `null`)
3940
* `response_code`: can be used to set the HTTP-StatusCode on a successful `OPTIONS` / Pre-Flight-Request (default: `204`)
4041
* `allow_credentials`: Configures the `Access-Control-Allow-Credentials` CORS header. Expects an boolean (ex: `true` // to set the header)
4142
* `allow_origin`: Configures the `Access-Control-Allow-Origin` CORS header. Expects an array (ex: `['http://example.net', 'https://example.net']`).
@@ -98,6 +99,22 @@ $server = new Server([
9899
]);
99100
```
100101

102+
## Use strict host checking
103+
104+
The default handling of this middleware will allow any "Host"-header. This means that you can use your server with
105+
any hostname you want. This might be a desired behavior but allows also the misuse of your server.
106+
107+
To prevent such a behavior there is a `server_url` option which will enable strict host checking. In this scenario
108+
the server will return a `403` with the body `Origin not allowed`.
109+
110+
```php
111+
$server = new Server([
112+
new CorsMiddleware([
113+
'server_url' => 'http://api.example.net:8080'
114+
]),
115+
]);
116+
```
117+
101118
# License
102119

103120
The MIT License (MIT)

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
},
2727
"require-dev": {
2828
"phpunit/phpunit": "^4.8.10||^5.0",
29-
"react/http": "^0.8.1",
29+
"react/http": "^0.8.6",
3030
"ringcentral/psr7": "^1.2"
3131
},
3232
"scripts": {

examples/01-server-with-cors.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
$server = new Server([
1414
new CorsMiddleware(),
15-
function (ServerRequestInterface $request, callable $next) {
15+
function (ServerRequestInterface $request) {
1616
return new Response(200, ['Content-Type' => 'application/json'], json_encode([
1717
'some' => 'nice',
1818
'json' => 'values',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Psr\Http\Message\ServerRequestInterface;
4+
use React\EventLoop\Factory;
5+
use React\Http\Response;
6+
use React\Http\Server;
7+
use Sikei\React\Http\Middleware\CorsMiddleware;
8+
9+
require __DIR__ . '/../vendor/autoload.php';
10+
11+
$loop = Factory::create();
12+
13+
$server = new Server([
14+
new CorsMiddleware(['server_url' => 'http://api.example.net:8080']),
15+
function (ServerRequestInterface $request) {
16+
return new Response(200, ['Content-Type' => 'application/json'], json_encode([
17+
'some' => 'nice',
18+
'json' => 'values',
19+
]));
20+
},
21+
]);
22+
23+
$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:8080', $loop);
24+
$server->listen($socket);
25+
26+
echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;
27+
28+
$loop->run();

src/CorsMiddlewareAnalysisStrategy.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@ public function __construct(CorsMiddlewareConfiguration $config = null)
1515
parent::__construct();
1616

1717
$this->config = $config;
18+
19+
$serverOrigin = $this->config->getServerOrigin();
20+
if (!empty($serverOrigin)) {
21+
$this
22+
->setCheckHost(true)
23+
->setServerOrigin($serverOrigin);
24+
}
25+
1826
$this
19-
// ->setCheckHost(true)
20-
// ->setServerOrigin($this->config->getServerOrigin())
2127
->setRequestCredentialsSupported($this->config->getRequestCredentialsSupported())
2228
->setRequestAllowedOrigins($this->config->getRequestAllowedOrigins())
2329
->setRequestAllowedMethods($this->config->getRequestAllowedMethods())

src/CorsMiddlewareConfiguration.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class CorsMiddlewareConfiguration
66
{
77

88
protected $settings = [
9+
'server_url' => null,
910
'response_code' => 204, // Pre-Flight Status Code
1011
'allow_credentials' => false,
1112
'allow_origin' => [],
@@ -16,9 +17,18 @@ class CorsMiddlewareConfiguration
1617
'max_age' => 60 * 60 * 24 * 20, // preflight request is valid for 20 days
1718
];
1819

20+
protected $serverOrigin = [];
21+
1922
public function __construct(array $settings = [])
2023
{
2124
$this->settings = array_merge($this->settings, $settings);
25+
26+
if (!is_null($this->settings['server_url'])) {
27+
$this->serverOrigin = parse_url($this->settings['server_url']);
28+
if (count(array_diff_key(['scheme' => '', 'host' => ''], $this->serverOrigin)) > 0) {
29+
throw new \InvalidArgumentException('Option "server_url" requires at least scheme and domain');
30+
}
31+
}
2232
}
2333

2434
public function getPreFlightResponseCode()
@@ -28,9 +38,7 @@ public function getPreFlightResponseCode()
2838

2939
public function getServerOrigin()
3040
{
31-
// @TODO: fixme up
32-
$origin = parse_url('http://api.my-cors.io:8001');
33-
return $origin;
41+
return $this->serverOrigin;
3442
}
3543

3644
public function getRequestCredentialsSupported()

tests/CorsMiddlewareTest.php

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,55 +24,70 @@ public function testTemplate()
2424

2525
/** @var PromiseInterface $result */
2626
$result = $middleware($request, $this->getNextCallback($response));
27-
$this->assertInstanceOf('React\Promise\Promise', $result);
27+
$this->assertInstanceOf(Promise::class, $result);
2828

2929
$result->then(function ($value) use (&$response) {
3030
$response = $value;
3131
});
32-
$this->assertInstanceOf('React\Http\Response', $response);
32+
$this->assertInstanceOf(Response::class, $response);
3333
}
3434

3535
public function testNoHostHeaderResponse()
3636
{
37-
$this->markTestSkipped('Not yet implemented');
38-
3937
$request = new ServerRequest('OPTIONS', 'https://api.example.net/', [
4038
'Origin' => 'https://www.example.net',
4139
'Access-Control-Request-Method' => 'GET',
4240
'Access-Control-Request-Headers' => 'Authorization',
4341
]);
4442
$request = $request->withoutHeader('Host');
43+
4544
$response = new Response(200, ['Content-Type' => 'text/html'], 'Some response');
4645

4746
$middleware = new CorsMiddleware([
4847
'allow_origin' => '*',
49-
'allow_methods' => ['OPTIONS'],
48+
'allow_methods' => ['GET'],
5049
]);
5150

5251
/** @var Response $result */
5352
$response = $middleware($request, $this->getNextCallback($response));
54-
$this->assertInstanceOf('React\Http\Response', $response);
55-
53+
$this->assertInstanceOf(Response::class, $response);
5654
$this->assertSame(401, $response->getStatusCode());
5755
}
5856

5957
public function testDefaultValuesShouldAllowRequest()
6058
{
61-
$request = new ServerRequest('GET', 'https://api.example.net/');
59+
$request = new ServerRequest('GET', 'https://api.example.net/', [
60+
'Origin' => 'https://api.example.net/'
61+
]);
6262
$response = new Response(200, ['Content-Type' => 'text/html'], 'Some response');
6363

64-
$middleware = new CorsMiddleware();
64+
$middleware = new CorsMiddleware(['server_url' => 'https://api.example.net/']);
6565

6666
/** @var PromiseInterface $promise */
6767
$promise = $middleware($request, $this->getNextCallback($response));
68-
$this->assertInstanceOf('React\Promise\Promise', $promise);
68+
$this->assertInstanceOf(Promise::class, $promise);
6969
$promise->then(function ($value) use (&$response) {
7070
$response = $value;
7171
});
72-
$this->assertInstanceOf('React\Http\Response', $response);
72+
$this->assertInstanceOf(Response::class, $response);
7373
$this->assertSame(200, $response->getStatusCode());
7474
}
7575

76+
public function testWrongHostShouldDenyRequest()
77+
{
78+
$request = new ServerRequest('GET', 'https://api.example.org/', [
79+
'Origin' => 'https://api.example.net/'
80+
]);
81+
$response = new Response(200, ['Content-Type' => 'text/html'], 'Some response');
82+
83+
$middleware = new CorsMiddleware(['server_url' => 'https://api.example.net/']);
84+
85+
/** @var PromiseInterface $promise */
86+
$response = $middleware($request, $this->getNextCallback($response));
87+
$this->assertInstanceOf(Response::class, $response);
88+
$this->assertSame(400, $response->getStatusCode());
89+
}
90+
7691
public function testDefaultValuesShouldDenyCrossOrigin()
7792
{
7893
$request = new ServerRequest('OPTIONS', 'https://api.example.net/', [
@@ -86,7 +101,7 @@ public function testDefaultValuesShouldDenyCrossOrigin()
86101

87102
/** @var Response $response */
88103
$response = $middleware($request, $this->getNextCallback($response));
89-
$this->assertInstanceOf('React\Http\Response', $response);
104+
$this->assertInstanceOf(Response::class, $response);
90105
$this->assertSame(403, $response->getStatusCode());
91106
}
92107

@@ -107,7 +122,7 @@ public function testRequestInvalidRequestHeaders()
107122

108123
/** @var Response $response */
109124
$response = $middleware($request, $this->getNextCallback($response));
110-
$this->assertInstanceOf('React\Http\Response', $response);
125+
$this->assertInstanceOf(Response::class, $response);
111126
$this->assertSame(401, $response->getStatusCode());
112127
}
113128

@@ -128,7 +143,7 @@ public function testRequestValidRequestHeaders()
128143

129144
/** @var Response $response */
130145
$response = $middleware($request, $this->getNextCallback($response));
131-
$this->assertInstanceOf('React\Http\Response', $response);
146+
$this->assertInstanceOf(Response::class, $response);
132147
$this->assertSame(204, $response->getStatusCode());
133148
}
134149

@@ -147,7 +162,7 @@ public function testRequestInvalidMethods()
147162

148163
/** @var Response $response */
149164
$response = $middleware($request, $this->getNextCallback($response));
150-
$this->assertInstanceOf('React\Http\Response', $response);
165+
$this->assertInstanceOf(Response::class, $response);
151166
$this->assertSame(405, $response->getStatusCode());
152167
}
153168

@@ -166,7 +181,7 @@ public function testRequestValidMethods()
166181

167182
/** @var Response $response */
168183
$response = $middleware($request, $this->getNextCallback($response));
169-
$this->assertInstanceOf('React\Http\Response', $response);
184+
$this->assertInstanceOf(Response::class, $response);
170185
$this->assertSame(204, $response->getStatusCode());
171186
}
172187

@@ -187,7 +202,7 @@ public function testRequestOriginByInvalidOrigin()
187202

188203
/** @var Response $response */
189204
$response = $middleware($request, $this->getNextCallback($response));
190-
$this->assertInstanceOf('React\Http\Response', $response);
205+
$this->assertInstanceOf(Response::class, $response);
191206
$this->assertSame(403, $response->getStatusCode());
192207
}
193208

@@ -208,7 +223,7 @@ public function testRequestOriginByValidOrigin()
208223

209224
/** @var Response $response */
210225
$response = $middleware($request, $this->getNextCallback($response));
211-
$this->assertInstanceOf('React\Http\Response', $response);
226+
$this->assertInstanceOf(Response::class, $response);
212227
$this->assertSame(204, $response->getStatusCode());
213228
}
214229

@@ -229,7 +244,7 @@ public function testRequestOriginByWildcard()
229244

230245
/** @var Response $response */
231246
$response = $middleware($request, $this->getNextCallback($response));
232-
$this->assertInstanceOf('React\Http\Response', $response);
247+
$this->assertInstanceOf(Response::class, $response);
233248
$this->assertSame(204, $response->getStatusCode());
234249

235250
// -- test wildcard as array
@@ -241,7 +256,7 @@ public function testRequestOriginByWildcard()
241256

242257
/** @var Response $response */
243258
$response = $middleware($request, $this->getNextCallback($response));
244-
$this->assertInstanceOf('React\Http\Response', $response);
259+
$this->assertInstanceOf(Response::class, $response);
245260
$this->assertSame(204, $response->getStatusCode());
246261
}
247262

@@ -265,7 +280,7 @@ public function testRequestOriginByPositiveCallback()
265280

266281
/** @var Response $response */
267282
$response = $middleware($request, $this->getNextCallback($response));
268-
$this->assertInstanceOf('React\Http\Response', $response);
283+
$this->assertInstanceOf(Response::class, $response);
269284
$this->assertSame(204, $response->getStatusCode());
270285
}
271286

@@ -287,7 +302,7 @@ public function testRequestOriginByNegativeCallback()
287302

288303
/** @var Response $response */
289304
$response = $middleware($request, $this->getNextCallback($response));
290-
$this->assertInstanceOf('React\Http\Response', $response);
305+
$this->assertInstanceOf(Response::class, $response);
291306
$this->assertSame(403, $response->getStatusCode());
292307
}
293308

@@ -309,7 +324,7 @@ public function testRequestOriginByInvalidCallbackReturn()
309324

310325
/** @var Response $response */
311326
$response = $middleware($request, $this->getNextCallback($response));
312-
$this->assertInstanceOf('React\Http\Response', $response);
327+
$this->assertInstanceOf(Response::class, $response);
313328
$this->assertSame(403, $response->getStatusCode());
314329
}
315330

@@ -329,7 +344,7 @@ public function testRequestCustomPreflightMaxAge()
329344

330345
/** @var Response $response */
331346
$response = $middleware($request, $this->getNextCallback($response));
332-
$this->assertInstanceOf('React\Http\Response', $response);
347+
$this->assertInstanceOf(Response::class, $response);
333348
$this->assertSame(204, $response->getStatusCode());
334349
$this->assertTrue($response->hasHeader('Access-Control-Max-Age'));
335350
$this->assertSame((string)3600, $response->getHeaderLine('Access-Control-Max-Age'));
@@ -351,7 +366,7 @@ public function testRequestCredentialsAllowed()
351366

352367
/** @var Response $response */
353368
$response = $middleware($request, $this->getNextCallback($response));
354-
$this->assertInstanceOf('React\Http\Response', $response);
369+
$this->assertInstanceOf(Response::class, $response);
355370
$this->assertSame(204, $response->getStatusCode());
356371
$this->assertTrue($response->hasHeader('Access-Control-Allow-Credentials'));
357372
$this->assertSame('true', strtolower($response->getHeaderLine('Access-Control-Allow-Credentials')));
@@ -373,7 +388,7 @@ public function testRequestCredentialsNotAllowed()
373388

374389
/** @var Response $response */
375390
$response = $middleware($request, $this->getNextCallback($response));
376-
$this->assertInstanceOf('React\Http\Response', $response);
391+
$this->assertInstanceOf(Response::class, $response);
377392
$this->assertSame(204, $response->getStatusCode());
378393
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
379394
}
@@ -394,7 +409,7 @@ public function testRequestCredentialsInvalidValueFallbackToFalse()
394409

395410
/** @var Response $response */
396411
$response = $middleware($request, $this->getNextCallback($response));
397-
$this->assertInstanceOf('React\Http\Response', $response);
412+
$this->assertInstanceOf(Response::class, $response);
398413
$this->assertSame(204, $response->getStatusCode());
399414
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
400415
}
@@ -419,7 +434,7 @@ public function testRequestExposedHeaderForResponseShouldBeHidden()
419434
$result->then(function ($value) use (&$response) {
420435
$response = $value;
421436
});
422-
$this->assertInstanceOf('React\Http\Response', $response);
437+
$this->assertInstanceOf(Response::class, $response);
423438

424439
$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
425440
}
@@ -443,7 +458,7 @@ public function testRequestExposedHeaderForResponseShouldBeVisible()
443458
$result->then(function ($value) use (&$response) {
444459
$response = $value;
445460
});
446-
$this->assertInstanceOf('React\Http\Response', $response);
461+
$this->assertInstanceOf(Response::class, $response);
447462

448463
$this->assertTrue($response->hasHeader('Access-Control-Expose-Headers'));
449464
$this->assertContains('X-Custom-Header', $response->getHeaderLine('Access-Control-Expose-Headers'));

0 commit comments

Comments
 (0)