diff --git a/system/typemill/Events/OnCspLoaded.php b/system/typemill/Events/OnCspLoaded.php new file mode 100644 index 0000000..cdae7ec --- /dev/null +++ b/system/typemill/Events/OnCspLoaded.php @@ -0,0 +1,14 @@ +settings = $settings; - $this->urlinfo = $urlinfo; + $this->$cspFromPlugins = $cspFromPlugins; + + $this->$cspFromTheme = $cspFromTheme; } public function process(Request $request, RequestHandler $handler) :response - { - $scheme = $request->getUri()->getScheme(); - + { # add the custom headers to the response after everything is processed $response = $handler->handle($request); - ################### - # CSP HEADER # - ################### + $whitelist = ["'unsafe-inline'", "'unsafe-eval'", "'self'", "*.youtube-nocookie.com", "*.youtube.com"]; $cspdomains = isset($this->settings['cspdomains']) ? trim($this->settings['cspdomains']) : false; - $defaultsrc = "default-src 'unsafe-inline' 'unsafe-eval' 'self'"; if($cspdomains && $cspdomains != '') { @@ -42,23 +41,29 @@ class CspHeadersMiddleware implements MiddlewareInterface $cspdomain = trim($cspdomain); if($cspdomain != '') { - $defaultsrc .= ' ' . $cspdomain; + $whitelist[] = $cspdomain; } } } - # dispatch to get from plugins - # get yaml from current theme + # add csp from plugins + if($this->cspFromPlugins && is_array($this->cspFromPlugins) && !empty($this->cspFromPlugins)) + { + $whitelist = array_merge($whitelist, $this->cspFromPlugins); + } + + # add csp from current theme + if($this->cspFromTheme && is_array($this->cspFromTheme) && !empty($this->cspFromTheme)) + { + $whitelist = array_merge($whitelist, $this->cspFromTheme); + } + + $whitelist = array_unique($whitelist); + $whitelist = implode(' ', $whitelist); # Define the Content Security Policy header - $cspHeader = $defaultsrc . ";"; // Default source is restricted to 'self' -# $cspHeader .= "frame-src 'self' https://www.youtube.com;"; // Allowing embedding YouTube videos -# $cspHeader .= "img-src *"; -# $cspHeader .= "media-src *"; -# $cspHeader .= "script-src *"; -# $cspHeader .= "style-src *"; -# $cspHeader .= "object-src 'none'"; - + $cspHeader = "default-src " . $whitelist . ";"; + # Add the Content Security Policy header to the response $response = $response->withHeader('Content-Security-Policy', $cspHeader); diff --git a/system/typemill/routes/api.php b/system/typemill/routes/api.php index da573f3..f47f867 100644 --- a/system/typemill/routes/api.php +++ b/system/typemill/routes/api.php @@ -92,7 +92,7 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { if(isset($routes['api']) && !empty($routes['api'])) { foreach($routes['api'] as $pluginRoute) - { + { $method = $pluginRoute['httpMethod'] ?? false; $route = $pluginRoute['route'] ?? false; $class = $pluginRoute['class'] ?? false; diff --git a/system/typemill/routes/web.php b/system/typemill/routes/web.php index 4024b0f..037269d 100644 --- a/system/typemill/routes/web.php +++ b/system/typemill/routes/web.php @@ -4,6 +4,7 @@ use Slim\Routing\RouteCollectorProxy; use Typemill\Middleware\WebRedirectIfAuthenticated; use Typemill\Middleware\WebRedirectIfUnauthenticated; use Typemill\Middleware\WebAuthorization; +use Typemill\Middleware\CspHeadersMiddleware; use Typemill\Controllers\ControllerWebSetup; use Typemill\Controllers\ControllerWebAuth; use Typemill\Controllers\ControllerWebRecover; @@ -31,7 +32,7 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($settings) { $group->post('/reset', ControllerWebRecover::class . ':resetPassword')->setName('auth.reset'); } -})->add(new WebRedirectIfAuthenticated($routeParser, $settings)); +})->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebRedirectIfAuthenticated($routeParser, $settings)); # AUTHOR AREA (requires authentication) $app->group('/tm', function (RouteCollectorProxy $group) use ($routeParser,$acl) { @@ -51,7 +52,7 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($routeParser,$acl) $group->get('/content/visual[/{route:.*}]', ControllerWebAuthor::class . ':showBlox')->setName('content.visual')->add(new WebAuthorization($routeParser, $acl, 'mycontent', 'view')); $group->get('/content/raw[/{route:.*}]', ControllerWebAuthor::class . ':showRaw')->setName('content.raw')->add(new WebAuthorization($routeParser, $acl, 'mycontent', 'view')); -})->add(new WebRedirectIfUnauthenticated($routeParser)); +})->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebRedirectIfUnauthenticated($routeParser)); $app->redirect('/tm', $routeParser->urlFor('auth.show'), 302); $app->redirect('/tm/', $routeParser->urlFor('auth.show'), 302); @@ -74,11 +75,11 @@ if(isset($routes['web']) && !empty($routes['web'])) if($resources && $privilege) { - $app->{$method}($route, $class)->setName($name)->add(new WebAuthorization($routeParser, $acl, $resource, $privilege))->add(new WebRedirectIfUnauthenticated($routeParser)); + $app->{$method}($route, $class)->setName($name)->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebAuthorization($routeParser, $acl, $resource, $privilege))->add(new WebRedirectIfUnauthenticated($routeParser)); } else { - $app->{$method}($route, $class)->setName($name); + $app->{$method}($route, $class)->setName($name)->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme)); } } } @@ -86,10 +87,10 @@ if(isset($routes['web']) && !empty($routes['web'])) if(isset($settings['access']) && $settings['access'] != '') { # if access for website is restricted - $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home')->add(new WebAuthorization($routeParser, $acl, 'account', 'view')); + $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home')->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebAuthorization($routeParser, $acl, 'account', 'view')); } else { # if access is not restricted - $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home'); + $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home')->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme)); } diff --git a/system/typemill/system.php b/system/typemill/system.php index 4602df9..70e30a8 100644 --- a/system/typemill/system.php +++ b/system/typemill/system.php @@ -21,6 +21,7 @@ use Typemill\Events\OnPluginsLoaded; use Typemill\Events\OnSessionSegmentsLoaded; use Typemill\Events\OnRolesPermissionsLoaded; use Typemill\Events\OnResourcesLoaded; +use Typemill\Events\OnCspLoaded; use Typemill\Middleware\RemoveCredentialsMiddleware; use Typemill\Middleware\SessionMiddleware; use Typemill\Middleware\OldInputMiddleware; @@ -378,9 +379,25 @@ if(isset($settings['proxy']) && $settings['proxy']) $timer['middleware'] = microtime(true); +/****************************** +* GET CSP FROM PLUGINS/THEMES * +******************************/ + +# get additional csp headers from plugins +$cspFromPlugins = $dispatcher->dispatch(new OnCspLoaded([]), 'onCspLoaded')->getData(); + +# get additional csp headers from theme +$cspFromTheme = []; +$themeSettings = $settingsModel->getObjectSettings('themes', $settings['theme']); +if(isset($themeSettings['csp']) && is_array($themeSettings['csp']) && !empty($themeSettings['csp']) ) +{ + $cspFromTheme = $themeSettings['csp']; +} + /************************ * ADD ROUTES * ************************/ + if(isset($settings['setup']) && $settings['setup'] == true) { require __DIR__ . '/routes/setup.php'; @@ -393,6 +410,7 @@ else $timer['routes'] = microtime(true); + /************************ * RUN APP * ************************/