Ver Fonte

tec: Allow to change CSP header from controllers

For an extension, I needed to call a script from an external domain.
Unfortunately, the CSP headers didn't allow this domain and I had to
patch manually the FreshRSS FrontController for my extension. It's
obviously not a long-term solution since it has nothing to do in the
core of FRSS, and I don't want to apply this patch manually at each
update.

With this patch, I allow changing the CSP header from inside the
controller actions. It allows extensions to modify headers. It's also an
opportunity to remove a bit of code from the FrontController. I wasn't
happy with the previous implementation anyhow.

Reference: https://github.com/flusio/xExtension-Flus/commit/ed12d56#diff-ff12e33ed31b23bda327499fa6e84eccR143
Marien Fressinaud há 6 anos atrás
pai
commit
7802fd33a6

+ 14 - 0
app/Controllers/indexController.php

@@ -83,6 +83,13 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 				Minz_Error::error(404);
 			}
 		};
+
+		$this->_csp([
+			'default-src' => "'self'",
+			'frame-src' => '*',
+			'img-src' => '* data:',
+			'media-src' => '*',
+		]);
 	}
 
 	/**
@@ -121,6 +128,13 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
 		}
 		Minz_View::prependTitle($title . ' · ');
+
+		$this->_csp([
+			'default-src' => "'self'",
+			'frame-src' => '*',
+			'img-src' => '* data:',
+			'media-src' => '*',
+		]);
 	}
 
 	/**

+ 5 - 0
app/Controllers/statsController.php

@@ -15,6 +15,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 			Minz_Error::error(403);
 		}
 
+		$this->_csp([
+			'default-src' => "'self'",
+			'style-src' => "'self' 'unsafe-inline'",
+		]);
+
 		Minz_View::prependTitle(_t('admin.stats.title') . ' · ');
 	}
 

+ 0 - 17
app/FreshRSS.php

@@ -124,23 +124,6 @@ class FreshRSS extends Minz_FrontController {
 	}
 
 	public static function preLayout() {
-		switch (Minz_Request::controllerName()) {
-			case 'index':
-				$urlToAuthorize = array_filter(array_map(function ($a) {
-					if (isset($a['method']) && $a['method'] === 'POST') {
-						return $a['url'];
-					}
-				}, FreshRSS_Context::$user_conf->sharing));
-				$connectSrc = count($urlToAuthorize) ? sprintf("; connect-src 'self' %s", implode(' ', $urlToAuthorize)) : '';
-				header(sprintf("Content-Security-Policy: default-src 'self'; frame-src *; img-src * data:; media-src *%s", $connectSrc));
-				break;
-			case 'stats':
-				header("Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'");
-				break;
-			default:
-				header("Content-Security-Policy: default-src 'self'");
-				break;
-		}
 		header("X-Content-Type-Options: nosniff");
 
 		FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));

+ 36 - 0
lib/Minz/ActionController.php

@@ -9,6 +9,9 @@
  */
 class Minz_ActionController {
 	protected $view;
+	private $csp_policies = array(
+		'default-src' => "'self'",
+	);
 
 	/**
 	 * Constructeur
@@ -27,6 +30,39 @@ class Minz_ActionController {
 		return $this->view;
 	}
 
+	/**
+	 * Set CSP policies.
+	 *
+	 * A default-src directive should always be given.
+	 *
+	 * References:
+	 * - https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
+	 * - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
+	 *
+	 * @param array $policies An array where keys are directives and values are sources.
+	 */
+	protected function _csp($policies) {
+		if (!isset($policies['default-src'])) {
+			$action = Minz_Request::controllerName() . '#' . Minz_Request::actionName();
+			Minz_Log::warning(
+				"Default CSP policy is not declared for action {$action}.",
+				ADMIN_LOG
+			);
+		}
+		$this->csp_policies = $policies;
+	}
+
+	/**
+	 * Send HTTP Content-Security-Policy header based on declared policies.
+	 */
+	public function declareCspHeader() {
+		$policies = [];
+		foreach ($this->csp_policies as $directive => $sources) {
+			$policies[] = $directive . ' ' . $sources;
+		}
+		header('Content-Security-Policy: ' . implode('; ', $policies));
+	}
+
 	/**
 	 * Méthodes à redéfinir (ou non) par héritage
 	 * firstAction est la première méthode exécutée par le Dispatcher

+ 1 - 0
lib/Minz/Dispatcher.php

@@ -50,6 +50,7 @@ class Minz_Dispatcher {
 				$this->controller->lastAction ();
 
 				if (!self::$needsReset) {
+					$this->controller->declareCspHeader();
 					$this->controller->view ()->build ();
 				}
 			} catch (Minz_Exception $e) {