Same-Origin Policy to norma w dzisiejszych przeglądarkach internetowych. Odpowiedzialna jest za bezpieczną wymianę zasobów i komunikację między dwoma aplikacjami. Chociaż w swoich założeniach obejmuje dość szeroki zakres to w praktyce, aktualnie, sprowadza się to do blokowania żądań wysyłanych z poziomu przeglądarki (AJAX) do API pod inną domeną (w uproszczeniu, bo w rzeczywistości chodzi o origin, na który składa się protokół, host i port). Mechanizm, który przez wielu uważany jest za zbędny, a już na pewno irytujący. Kiedy jednak zostanie prawidłowo zrozumiany, okazuje się że nie jest taki głupi.
Jaki więc spełnia cel? Nawet jeśli nie widać zagrożenia wynikającego z możliwości wykonania zapytania z poziomu dowolnej domeny (bo przecież API jest odpowiednio autoryzowane) to istnieją innego rodzaju ataki. Głównie chodzi o CSRF (Cross-Site Request Forgery), który pozwala wykorzystać użytkownika posiadającego uprawnienia do wykonania żądania tak by je nieświadomie uskutecznił. Warto też dodać, że sam CSRF zostanie jedynie ograniczony przez SOP. W połączeniu z podatnością typu XSS (Cross-Site Scripting) nadal istnieje opcja jego wykorzystania. W praktyce nie powinno to więc być jedyne zabezpieczenie przed tym atakiem – zazwyczaj dokłada się jeszcze token.
Trzeba jednak przyznać, że polityka SOP jest zbyt restrykcyjna. Nie ma możliwości tworzenia nowoczesnych aplikacji bez komunikacji między różnymi serwisami. Na szczęście z pomocą przychodzi tytułowy CORS (Cross-Origin Resource Sharing), czyli mechanizm, który pozwala na kontrolowany dostęp do zasobów innej domeny. Dzięki niemu można zdefiniować originy, które otrzymają dostęp do zasobu i reprezentują zaufane domeny.
Jak działa CORS z poziomu przeglądarki? Narzędzie wykonuje tak zwany preflight request, który sprawdzi, czy z danego originu komunikacja jest możliwa. Implementacja jest już gotowa, więc dzieje się to automatycznie i nie wymaga żadnego dodatkowego kodu po stronie aplikacji. Jedną z możliwości jest wildcard (*), czyli czyli całkowite pozbycie się polityki Same-Origin Policy i pozwolenie każdemu na wykonywanie żądań XHR. Jeśli ktoś podejmuje taką decyzję świadomie i faktycznie jest to niezbędne to w porządku, ale częściej jednak powinno się to sprowadzać do kontrolowanych dostępów. Tak zresztą będzie w tej aplikacji.
Implementacja CORS w PHP z Mezzio
Tym razem więcej teorii, bo kodu będzie bardzo mało. Do obsługi Cross-Origin Resource Sharing wykorzystany zostanie gotowy moduł z pakietu Mezzio. Jego instalację można zrealizować za pomocą poniższej komendy.
composer require mezzio/mezzio-cors
Automatycznie, bądź samodzielnie trzeba dorzucić providera odpowiedzialnego za konfigurację w pliku config/config.php
. Następnie zarejestrować middleware realizujący CORS w pliku config/pipeline.php
. Dodany zostaje tuż pod tym odpowiedzialnym za obsługę błędów. Obie czynności zostały zaznaczone w poniższych blokach kodu.
<?php
declare(strict_types=1);
use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ConfigAggregator\PhpFileProvider;
$cacheConfig = [
'config_cache_path' => 'data/cache/config-cache.php',
];
$aggregator = new ConfigAggregator([
Mezzio\Cors\ConfigProvider::class,
Laminas\InputFilter\ConfigProvider::class,
Laminas\Filter\ConfigProvider::class,
Laminas\Validator\ConfigProvider::class,
Mezzio\Authentication\Basic\ConfigProvider::class,
Mezzio\Authentication\ConfigProvider::class,
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
Laminas\HttpHandlerRunner\ConfigProvider::class,
new ArrayProvider($cacheConfig),
Mezzio\Helper\ConfigProvider::class,
Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class,
Laminas\Diactoros\ConfigProvider::class,
Error\ConfigProvider::class,
Auth\ConfigProvider::class,
Store\ConfigProvider::class,
Customer\ConfigProvider::class,
new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
], $cacheConfig['config_cache_path']);
return $aggregator->getMergedConfig();
<?php
declare(strict_types=1);
use Auth\Middleware\IpAccessControlMiddleware;
use Laminas\Stratigility\Middleware\ErrorHandler;
use Mezzio\Application;
use Mezzio\Authentication\AuthenticationMiddleware;
use Mezzio\Cors\Middleware\CorsMiddleware;
use Mezzio\Handler\NotFoundHandler;
use Mezzio\Helper\BodyParams\BodyParamsMiddleware;
use Mezzio\Helper\ServerUrlMiddleware;
use Mezzio\Helper\UrlHelperMiddleware;
use Mezzio\MiddlewareFactory;
use Mezzio\Router\Middleware\DispatchMiddleware;
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
use Mezzio\Router\Middleware\ImplicitOptionsMiddleware;
use Mezzio\Router\Middleware\MethodNotAllowedMiddleware;
use Mezzio\Router\Middleware\RouteMiddleware;
use Psr\Container\ContainerInterface;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void {
$app->pipe(ErrorHandler::class);
$app->pipe(CorsMiddleware::class);
$app->pipe(ServerUrlMiddleware::class);
$app->pipe('/api/v1', AuthenticationMiddleware::class);
$app->pipe('/api/v1', IpAccessControlMiddleware::class);
$app->pipe(RouteMiddleware::class);
$app->pipe(ImplicitHeadMiddleware::class);
$app->pipe(ImplicitOptionsMiddleware::class);
$app->pipe(MethodNotAllowedMiddleware::class);
$app->pipe(BodyParamsMiddleware::class);
$app->pipe(UrlHelperMiddleware::class);
$app->pipe(DispatchMiddleware::class);
$app->pipe(NotFoundHandler::class);
};
Jeśli chodzi o konfigurację samego modułu to już wszystko. Teraz czas na implementację, ale okazuje się że moduł nie wymaga żadnego dodatkowego kodu. Jedyne co to prostą konfigurację. Prostą przynajmniej w tym przypadku, bo oczywiście CORS może być definiowany bardzo szeroko i nawet narzucać różne zasady dla różnych końcówek API. Jak zwykle odsyłam do dokumentacji biblioteki.
<?php
declare(strict_types=1);
use Mezzio\Cors\Configuration\ConfigurationInterface;
return [
ConfigurationInterface::CONFIGURATION_IDENTIFIER => [
'allowed_headers' => ['Authorization'],
'allowed_max_age' => '3600',
'credentials_allowed' => true,
'exposed_headers' => []
],
];
<?php
declare(strict_types=1);
use Mezzio\Cors\Configuration\ConfigurationInterface;
return [
ConfigurationInterface::CONFIGURATION_IDENTIFIER => [
'allowed_origins' => ['https://koddlo.pl', 'https://*.local']
]
];
Powyżej znajdują się dwa pliki. Jeden to config/autoload/cors.global.php
z konfiguracją dla całej aplikacji, a drugi to config/autoload/cors.local.php
z konfiguracją dla odpowiedniego środowiska. Oczywiście ten drugi nie trafia do repozytorium kodu – jedynie jego szablon. Dzięki takiemu rozwiązaniu można definiować zaufane domeny pod konkretne środowisko i ograniczyć je do minimum. Dodatkowo w przypadku potrzeby dodania nowej, wystarczy ją uwzględnić w pliku – nie trzeba wprowadzać zmian w repozytorium kodu.
Krótko jeszcze o opcjach konfiguracyjnych:
- allowed_origins: definiuje dozwolone originy po przecinku,
- allowed_headers: definiuje dozwolone nagłówki przesyłane w żądaniu do serwera,
- allowed_max_age: określa, jak długo preflight request może żyć w cache,
- credentials_allowed: określa, czy mogą być przesyłane dane dostępowe,
- exposed_headers: definiuje dozwolone nagłówki przesyłane w odpowiedzi serwera (poza kilkoma, które są w puli domyślnie).
Podsumowanie – CORS w Laminas Mezzio
Nie uwierzycie, ale CORS już działa. Teraz żądania z poziomu przeglądarki do API z bazą klientów będą możliwe tylko ze zdefiniowanych źródeł. SOP i CORS to dość uciążliwe mechanizmy, ale pomagające podnieść jakość bezpieczeństwa aplikacji, więc nie warto z nich rezygnować. Zresztą jak widzicie, implementacja mechanizmu po stronie serwera jest niezwykle prosta – przynajmniej w tak podstawowej wersji jak w tym wpisie.
Zachęcam do przeczytania jeszcze więcej o tym zagadnieniu. Dwa dość obszerne wpisy znajdziecie w serwisach Sekurak i Mansfeld.
Cały kod dostępny jako gałąź step07.
Odpowiedz