Browse Source

CORS, trusted domain

Roman Hergenreder 3 weeks ago
parent
commit
3851b7f289

+ 3 - 0
Core/API/LogsAPI.class.php

@@ -105,6 +105,8 @@ namespace Core\API\Logs {
                 "message" => $content,
                 "timestamp" => $date->format(Parameter::DATE_TIME_FORMAT)
               ];
+
+              $this->result["pagination"]["total"] += 1;
             }
           }
         }
@@ -139,6 +141,7 @@ namespace Core\API\Logs {
             "timestamp" => (new \DateTime())->format(Parameter::DATE_TIME_FORMAT)
           ]
         ];
+        $this->result["pagination"]["total"] += 1;
       }
 
       $this->loadFromFileSystem($this->result["logs"]);

+ 12 - 0
Core/API/Request.class.php

@@ -161,8 +161,20 @@ abstract class Request {
     return true;
   }
 
+  protected function getCORS(): array {
+    $settings = $this->context->getSettings();
+    return $settings->getTrustedDomains();
+  }
+
   public final function execute($values = array()): bool {
 
+    if ($this->externalCall) {
+      $trustedDomains = $this->getCORS();
+      if (!empty($trustedDomains)) {
+        header("Access-Control-Allow-Origin: " . implode(", ", $trustedDomains));
+      }
+    }
+
     $this->params = array_merge([], $this->defaultParams);
     $this->success = false;
     $this->result = array();

+ 1 - 0
Core/API/SettingsAPI.class.php

@@ -53,6 +53,7 @@ namespace Core\API\Settings {
     }
   }
 
+  // TODO: we need additional validation for built-in settings here, e.g. csv-values, bool values, etc.
   class Set extends SettingsAPI {
     public function __construct(Context $context, bool $externalCall = false) {
       parent::__construct($context, $externalCall, array(

+ 4 - 1
Core/API/Swagger.class.php

@@ -13,9 +13,12 @@ class Swagger extends Request {
     $this->csrfTokenRequired = false;
   }
 
+  protected function getCORS(): array {
+    return ["*"];
+  }
+
   public function _execute(): bool {
     header("Content-Type: application/x-yaml");
-    header("Access-Control-Allow-Origin: *");
     die($this->getDocumentation());
   }
 

+ 28 - 2
Core/Configuration/Settings.class.php

@@ -22,6 +22,7 @@ class Settings {
   // general settings
   private string $siteName;
   private string $baseUrl;
+  private array $trustedDomains;
   private bool $registrationAllowed;
   private array $allowedExtensions;
   private string $timeZone;
@@ -45,7 +46,7 @@ class Settings {
   }
 
   public static function getAll(?SQL $sql, ?string $pattern = null, bool $external = false): ?array {
-    $query = $sql->select("name", "value") ->from("Settings");
+    $query = $sql->select("name", "value")->from("Settings");
 
     if ($pattern) {
       $query->where(new CondRegex(new Column("name"), $pattern));
@@ -91,6 +92,7 @@ class Settings {
     // General
     $settings->siteName = "WebBase";
     $settings->baseUrl = "$protocol://$hostname";
+    $settings->trustedDomains = [$hostname];
     $settings->allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'htm', 'html'];
     $settings->installationComplete = false;
     $settings->registrationAllowed = false;
@@ -130,6 +132,7 @@ class Settings {
       $this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
       $this->mailAsync = $result["mail_async"] ?? $this->mailAsync;
       $this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions)));
+      $this->trustedDomains = explode(",", $result["trusted_domains"] ?? strtolower(implode(",", $this->trustedDomains)));
       date_default_timezone_set($this->timeZone);
     }
 
@@ -139,13 +142,14 @@ class Settings {
   public function addRows(Insert $query): void {
     $query->addRow("site_name", $this->siteName, false, false)
       ->addRow("base_url", $this->baseUrl, false, false)
+      ->addRow("trusted_domains", implode(",", $this->trustedDomains), false, false)
       ->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
       ->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
       ->addRow("time_zone", $this->timeZone, false, false)
       ->addRow("recaptcha_enabled", $this->recaptchaEnabled ? "1" : "0", false, false)
       ->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false)
       ->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false)
-      ->addRow("allowed_extensions", implode(",", $this->allowedExtensions), true, false)
+      ->addRow("allowed_extensions", implode(",", $this->allowedExtensions), false, false)
       ->addRow("mail_host", "", false, false)
       ->addRow("mail_port", "", false, false)
       ->addRow("mail_username", "", false, false)
@@ -211,4 +215,26 @@ class Settings {
   public function getLogger(): Logger {
     return $this->logger;
   }
+
+  public function isTrustedDomain(string $domain): bool {
+    $domain = strtolower($domain);
+    foreach ($this->trustedDomains as $trustedDomain) {
+      $trustedDomain = trim(strtolower($trustedDomain));
+      if ($trustedDomain === $domain) {
+        return true;
+      }
+
+
+      // *.def.com <-> abc.def.com
+      if (startsWith($trustedDomain, "*.") && endsWith($domain, substr($trustedDomain, 1))) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  public function getTrustedDomains(): array {
+    return $this->trustedDomains;
+  }
 }

+ 1 - 3
Core/Elements/Document.class.php

@@ -21,7 +21,6 @@ abstract class Document {
   private bool $cspEnabled;
   private ?string $cspNonce;
   private array $cspWhitelist;
-  private string $domain;
   protected bool $searchable;
   protected array $languageModules;
 
@@ -31,7 +30,6 @@ abstract class Document {
     $this->cspNonce = null;
     $this->databaseRequired = true;
     $this->cspWhitelist = [];
-    $this->domain = $this->getSettings()->getBaseUrl();
     $this->logger = new Logger("Document", $this->getSQL());
     $this->searchable = false;
     $this->languageModules = ["general"];
@@ -83,7 +81,7 @@ abstract class Document {
   public function addCSPWhitelist(string $path) {
     $urlParts = parse_url($path);
     if (!$urlParts || !isset($urlParts["host"])) {
-      $this->cspWhitelist[] = $this->domain . $path;
+      $this->cspWhitelist[] = getProtocol() . "://" . getCurrentHostName() . $path;
     } else {
       $this->cspWhitelist[] = $path;
     }

+ 1 - 0
Core/Localization/de_DE/settings.php

@@ -25,6 +25,7 @@ return [
   "base_url" => "Basis URL",
   "user_registration_enabled" => "Benutzerregistrierung erlauben",
   "allowed_extensions" => "Erlaubte Dateierweiterungen",
+  "trusted_domains" => "Vertraute Ursprungs-Domains (Komma getrennt, * als Subdomain-Wildcard)",
   "time_zone" => "Zeitzone",
 
   # mail settings

+ 1 - 0
Core/Localization/en_US/settings.php

@@ -25,6 +25,7 @@ return [
   "base_url" => "Base URL",
   "user_registration_enabled" => "Allow user registration",
   "allowed_extensions" => "Allowed file extensions",
+  "trusted_domains" => "Trusted origin domains (comma separated, * as subdomain-wildcard)",
   "time_zone" => "Time zone",
 
   # mail settings

+ 6 - 5
Core/Objects/Router/Router.class.php

@@ -40,21 +40,22 @@ class Router {
     return $this->requestedUri;
   }
 
-  public function run(string $url): string {
+  public function run(string $url, array &$pathParams): ?Route {
 
     // TODO: do we want a global try cache and return status page 500 on any error?
     $this->requestedUri = $url;
 
     $url = strtok($url, "?");
     foreach ($this->routes as $route) {
-      $pathParams = $route->match($url);
-      if ($pathParams !== false) {
+      $match = $route->match($url);
+      if ($match !== false) {
         $this->activeRoute = $route;
-        return $route->call($this, $pathParams);
+        $pathParams = $match;
+        return $this->activeRoute;
       }
     }
 
-    return $this->returnStatusCode(404);
+    return null;
   }
 
   public function returnStatusCode(int $code, array $params = []): string {

+ 1 - 1
cli.php

@@ -959,7 +959,7 @@ $registeredCommands = [
   "mail" => ["handler" => "onMail", "description" => "send mails and process the pipeline", "requiresDocker" => true],
   "settings" => ["handler" => "onSettings", "description" => "change and view settings"],
   "impersonate" => ["handler" => "onImpersonate", "description" => "create a session and print cookies and csrf tokens", "requiresDocker" => true],
-  "frontend" => ["handler" => "onFrontend", "description" => "build and manage frontend modules", "requiresDocker" => true],
+  "frontend" => ["handler" => "onFrontend", "description" => "build and manage frontend modules"],
   "api" => ["handler" => "onAPI", "description" => "view and create API endpoints"],
 ];
 

+ 20 - 2
index.php

@@ -18,7 +18,8 @@ use Core\Objects\Router\Router;
 
 if (!is_readable(getClassPath(Configuration::class))) {
   header("Content-Type: application/json");
-  die(json_encode([ "success" => false, "msg" => "Configuration class is not readable, check permissions before proceeding." ]));
+  http_response_code(500);
+  die(json_encode(createError("Configuration class is not readable, check permissions before proceeding.")));
 }
 
 $context = Context::instance();
@@ -26,6 +27,8 @@ $sql = $context->initSQL();
 $settings = $context->getSettings();
 $context->parseCookies();
 
+$currentHostName = getCurrentHostName();
+
 $installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
 $requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"];
 
@@ -61,12 +64,27 @@ if ($installation) {
   }
 
   if ($router !== null) {
+
     if ((!isset($_GET["site"]) || $_GET["site"] === "/") && isset($_GET["error"]) &&
       is_string($_GET["error"]) && preg_match("/^\d+$/", $_GET["error"])) {
       $response = $router->returnStatusCode(intval($_GET["error"]));
     } else {
       try {
-        $response = $router->run($requestedUri);
+        $pathParams = [];
+        $route = $router->run($requestedUri, $pathParams);
+        if ($route === null) {
+          $response = $router->returnStatusCode(404);
+        } else if (!$settings->isTrustedDomain($currentHostName)) {
+          if ($route instanceof \Core\Objects\Router\ApiRoute) {
+            header("Content-Type: application/json");
+            http_response_code(403);
+            $response = json_encode(createError("Untrusted Origin"));
+          } else {
+            $response = $router->returnStatusCode(403, ["message" => "Untrusted Origin"]);
+          }
+        } else {
+          $response = $route->call($router, $pathParams);
+        }
       } catch (\Throwable $e) {
         http_response_code(500);
         $router->getLogger()->error($e->getMessage());

+ 2 - 0
react/admin-panel/src/views/settings.js

@@ -46,6 +46,7 @@ export default function SettingsView(props) {
           "user_registration_enabled",
           "time_zone",
           "allowed_extensions",
+          "trusted_domains",
       ],
       "mail": [
           "mail_enabled",
@@ -275,6 +276,7 @@ export default function SettingsView(props) {
             return [
                 renderTextInput("site_name"),
                 renderTextInput("base_url"),
+                renderTextInput("trusted_domains"),
                 renderCheckBox("user_registration_enabled"),
                 renderTextInput("allowed_extensions"),
                 renderSelection("time_zone", TIME_ZONES),