CORS, trusted domain

This commit is contained in:
Roman Hergenreder 2024-04-11 11:51:50 -04:00
parent a238ad3b7f
commit 3851b7f289
12 changed files with 80 additions and 14 deletions

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

@ -161,8 +161,20 @@ abstract class Request {
return true; return true;
} }
protected function getCORS(): array {
$settings = $this->context->getSettings();
return $settings->getTrustedDomains();
}
public final function execute($values = array()): bool { 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->params = array_merge([], $this->defaultParams);
$this->success = false; $this->success = false;
$this->result = array(); $this->result = array();

@ -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 { class Set extends SettingsAPI {
public function __construct(Context $context, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array( parent::__construct($context, $externalCall, array(

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

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

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

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

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

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

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

@ -18,7 +18,8 @@ use Core\Objects\Router\Router;
if (!is_readable(getClassPath(Configuration::class))) { if (!is_readable(getClassPath(Configuration::class))) {
header("Content-Type: application/json"); 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(); $context = Context::instance();
@ -26,6 +27,8 @@ $sql = $context->initSQL();
$settings = $context->getSettings(); $settings = $context->getSettings();
$context->parseCookies(); $context->parseCookies();
$currentHostName = getCurrentHostName();
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled()); $installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
$requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"]; $requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"];
@ -61,12 +64,27 @@ if ($installation) {
} }
if ($router !== null) { if ($router !== null) {
if ((!isset($_GET["site"]) || $_GET["site"] === "/") && isset($_GET["error"]) && if ((!isset($_GET["site"]) || $_GET["site"] === "/") && isset($_GET["error"]) &&
is_string($_GET["error"]) && preg_match("/^\d+$/", $_GET["error"])) { is_string($_GET["error"]) && preg_match("/^\d+$/", $_GET["error"])) {
$response = $router->returnStatusCode(intval($_GET["error"])); $response = $router->returnStatusCode(intval($_GET["error"]));
} else { } else {
try { 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) { } catch (\Throwable $e) {
http_response_code(500); http_response_code(500);
$router->getLogger()->error($e->getMessage()); $router->getLogger()->error($e->getMessage());

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