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,
"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"]);

@ -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();

@ -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(

@ -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());
}

@ -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;
}
}

@ -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;
}

@ -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

@ -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

@ -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 {

@ -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"],
];

@ -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());

@ -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),