Skip to content

Commit

Permalink
Merge pull request #8 from christoph-kluge/custom-origin
Browse files Browse the repository at this point in the history
Feature: Add ability to enable strict host checking
  • Loading branch information
christoph-kluge authored Apr 18, 2020
2 parents bdd79c3 + 687ac18 commit d61eb96
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 36 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The defaults for this middleware are mainly taken from [enable-cors.org](https:/

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

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

## Use strict host checking

The default handling of this middleware will allow any "Host"-header. This means that you can use your server with
any hostname you want. This might be a desired behavior but allows also the misuse of your server.

To prevent such a behavior there is a `server_url` option which will enable strict host checking. In this scenario
the server will return a `403` with the body `Origin not allowed`.

```php
$server = new Server([
new CorsMiddleware([
'server_url' => 'http://api.example.net:8080'
]),
]);
```

# License

The MIT License (MIT)
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"require-dev": {
"phpunit/phpunit": "^4.8.10||^5.0",
"react/http": "^0.8.1",
"react/http": "^0.8.6",
"ringcentral/psr7": "^1.2"
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion examples/01-server-with-cors.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

$server = new Server([
new CorsMiddleware(),
function (ServerRequestInterface $request, callable $next) {
function (ServerRequestInterface $request) {
return new Response(200, ['Content-Type' => 'application/json'], json_encode([
'some' => 'nice',
'json' => 'values',
Expand Down
28 changes: 28 additions & 0 deletions examples/02-server-with-cors-strict-checks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Factory;
use React\Http\Response;
use React\Http\Server;
use Sikei\React\Http\Middleware\CorsMiddleware;

require __DIR__ . '/../vendor/autoload.php';

$loop = Factory::create();

$server = new Server([
new CorsMiddleware(['server_url' => 'http://api.example.net:8080']),
function (ServerRequestInterface $request) {
return new Response(200, ['Content-Type' => 'application/json'], json_encode([
'some' => 'nice',
'json' => 'values',
]));
},
]);

$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:8080', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;

$loop->run();
10 changes: 8 additions & 2 deletions src/CorsMiddlewareAnalysisStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ public function __construct(CorsMiddlewareConfiguration $config = null)
parent::__construct();

$this->config = $config;

$serverOrigin = $this->config->getServerOrigin();
if (!empty($serverOrigin)) {
$this
->setCheckHost(true)
->setServerOrigin($serverOrigin);
}

$this
// ->setCheckHost(true)
// ->setServerOrigin($this->config->getServerOrigin())
->setRequestCredentialsSupported($this->config->getRequestCredentialsSupported())
->setRequestAllowedOrigins($this->config->getRequestAllowedOrigins())
->setRequestAllowedMethods($this->config->getRequestAllowedMethods())
Expand Down
14 changes: 11 additions & 3 deletions src/CorsMiddlewareConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class CorsMiddlewareConfiguration
{

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

protected $serverOrigin = [];

public function __construct(array $settings = [])
{
$this->settings = array_merge($this->settings, $settings);

if (!is_null($this->settings['server_url'])) {
$this->serverOrigin = parse_url($this->settings['server_url']);
if (count(array_diff_key(['scheme' => '', 'host' => ''], $this->serverOrigin)) > 0) {
throw new \InvalidArgumentException('Option "server_url" requires at least scheme and domain');
}
}
}

public function getPreFlightResponseCode()
Expand All @@ -28,9 +38,7 @@ public function getPreFlightResponseCode()

public function getServerOrigin()
{
// @TODO: fixme up
$origin = parse_url('http://api.my-cors.io:8001');
return $origin;
return $this->serverOrigin;
}

public function getRequestCredentialsSupported()
Expand Down
73 changes: 44 additions & 29 deletions tests/CorsMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,55 +24,70 @@ public function testTemplate()

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

$result->then(function ($value) use (&$response) {
$response = $value;
});
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
}

public function testNoHostHeaderResponse()
{
$this->markTestSkipped('Not yet implemented');

$request = new ServerRequest('OPTIONS', 'https://api.example.net/', [
'Origin' => 'https://www.example.net',
'Access-Control-Request-Method' => 'GET',
'Access-Control-Request-Headers' => 'Authorization',
]);
$request = $request->withoutHeader('Host');

$response = new Response(200, ['Content-Type' => 'text/html'], 'Some response');

$middleware = new CorsMiddleware([
'allow_origin' => '*',
'allow_methods' => ['OPTIONS'],
'allow_methods' => ['GET'],
]);

/** @var Response $result */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);

$this->assertInstanceOf(Response::class, $response);
$this->assertSame(401, $response->getStatusCode());
}

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

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

/** @var PromiseInterface $promise */
$promise = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Promise\Promise', $promise);
$this->assertInstanceOf(Promise::class, $promise);
$promise->then(function ($value) use (&$response) {
$response = $value;
});
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(200, $response->getStatusCode());
}

public function testWrongHostShouldDenyRequest()
{
$request = new ServerRequest('GET', 'https://api.example.org/', [
'Origin' => 'https://api.example.net/'
]);
$response = new Response(200, ['Content-Type' => 'text/html'], 'Some response');

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

/** @var PromiseInterface $promise */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(400, $response->getStatusCode());
}

public function testDefaultValuesShouldDenyCrossOrigin()
{
$request = new ServerRequest('OPTIONS', 'https://api.example.net/', [
Expand All @@ -86,7 +101,7 @@ public function testDefaultValuesShouldDenyCrossOrigin()

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(403, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(401, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(204, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(405, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(204, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(403, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(204, $response->getStatusCode());
}

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

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

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(204, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(204, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(403, $response->getStatusCode());
}

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(403, $response->getStatusCode());
}

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

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

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

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

/** @var Response $response */
$response = $middleware($request, $this->getNextCallback($response));
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(204, $response->getStatusCode());
$this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials'));
}
Expand All @@ -419,7 +434,7 @@ public function testRequestExposedHeaderForResponseShouldBeHidden()
$result->then(function ($value) use (&$response) {
$response = $value;
});
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);

$this->assertFalse($response->hasHeader('Access-Control-Expose-Headers'));
}
Expand All @@ -443,7 +458,7 @@ public function testRequestExposedHeaderForResponseShouldBeVisible()
$result->then(function ($value) use (&$response) {
$response = $value;
});
$this->assertInstanceOf('React\Http\Response', $response);
$this->assertInstanceOf(Response::class, $response);

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

0 comments on commit d61eb96

Please sign in to comment.