From cde87db064722adbd943819d12b5418f18f8e44a Mon Sep 17 00:00:00 2001 From: Philipp Urbschat Date: Sat, 12 Sep 2026 22:14:58 +0200 Subject: [PATCH] security: protect API keys with backend proxy, harden auth, htaccess and session management --- .htaccess | 15 +- auth.php | 1 + dinos/api_proxy.php | 92 ++ dinos/auth.php | 9 + dinos/index.php | 86 +- dinos/test.html | 1049 ---------------------- home/app/controllers/LoginController.php | 2 + home/app/init.php | 1 + home/core/Controller.php | 10 +- home/core/Router.php | 5 +- home/core/Security.php | 2 +- home/public/index.php | 5 +- philcore/app/init.php | 7 +- philcore/core/Controller.php | 18 +- philcore/core/Router.php | 5 +- philcore/core/Security.php | 2 +- plants/api.php | 4 +- plants/auth_api.php | 3 +- plants/test.html | 82 -- plants/test_prompt.php | 228 ----- test/index.html | 22 - 21 files changed, 190 insertions(+), 1458 deletions(-) create mode 100644 dinos/api_proxy.php delete mode 100644 dinos/test.html delete mode 100644 plants/test.html delete mode 100644 plants/test_prompt.php delete mode 100644 test/index.html diff --git a/.htaccess b/.htaccess index 272f275..f40a73c 100644 --- a/.htaccess +++ b/.htaccess @@ -2,17 +2,26 @@ Options -Indexes RewriteEngine On -# Interne PHP- und Template-Verzeichnisse vor direktem Webzugriff sperren +# HTTP Security Headers + + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-Content-Type-Options "nosniff" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + + +# Interne PHP-, Vendor- und Template-Verzeichnisse vor direktem Webzugriff sperren RewriteRule ^(home|philcore)/(app|core|views|src)/ - [F,L] +RewriteRule ^(.*/)?vendor/ - [F,L] # Sensible Dateien und Verzeichnisse vor direktem Webzugriff schützen Require all denied - +# Interne Hilfsdateien vor direktem Aufruf schützen + Require all denied - + # 1. Assets aus home/public weiterleiten (falls direkt aufgerufen wie /css/style.css) RewriteCond %{REQUEST_FILENAME} !-f diff --git a/auth.php b/auth.php index 95a3f21..6a41921 100644 --- a/auth.php +++ b/auth.php @@ -6,6 +6,7 @@ if (session_status() === PHP_SESSION_NONE) { session_set_cookie_params([ 'path' => '/', 'httponly' => true, + 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), 'samesite' => 'Lax' // Lax erlaubt den Wechsel zwischen /home und /test ]); session_start(); diff --git a/dinos/api_proxy.php b/dinos/api_proxy.php new file mode 100644 index 0000000..3500eb5 --- /dev/null +++ b/dinos/api_proxy.php @@ -0,0 +1,92 @@ + 'Nur POST-Anfragen sind erlaubt.']); + exit; +} + +// 1. API-Schlüssel sicher aus .env laden +$apiKey = ''; +$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env']; +foreach ($envFiles as $envFile) { + if (file_exists($envFile)) { + foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') continue; + if (strpos($line, '=') !== false) { + list($k, $v) = explode('=', $line, 2); + if (trim($k) === 'GEMINI_API_KEY') { + $apiKey = trim(trim($v), '"\''); + break 2; + } + } + } + } +} + +if (empty($apiKey)) { + http_response_code(500); + echo json_encode(['error' => 'API-Schlüssel nicht konfiguriert.']); + exit; +} + +// 2. JSON-Daten empfangen +$json_input = file_get_contents('php://input'); +$request_data = json_decode($json_input, true); + +if (json_last_error() !== JSON_ERROR_NONE || !isset($request_data['apiUrl']) || !isset($request_data['payload'])) { + http_response_code(400); + echo json_encode(['error' => 'Ungültige Anfrage-Daten.']); + exit; +} + +$apiUrl = $request_data['apiUrl']; +$payload = $request_data['payload']; + +// 3. SSRF-Schutz: Nur generativelanguage.googleapis.com erlauben +$parsed = parse_url($apiUrl); +if (!isset($parsed['host']) || $parsed['host'] !== 'generativelanguage.googleapis.com') { + http_response_code(403); + echo json_encode(['error' => 'Ungültiges API-Ziel.']); + exit; +} + +// 4. Ziel-URL mit API-Schlüssel zusammensetzen +$separator = (strpos($apiUrl, '?') !== false) ? '&' : '?'; +$fullApiUrl = $apiUrl . $separator . 'key=' . $apiKey; + +// 5. Anfrage mit cURL an Google API senden +$ch = curl_init(); + +curl_setopt_array($ch, [ + CURLOPT_URL => $fullApiUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + ], + CURLOPT_TIMEOUT => 90, +]); + +$response_body = curl_exec($ch); +$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + +if (curl_errno($ch)) { + http_response_code(500); + echo json_encode(['error' => 'Fehler bei Weiterleitung an Google: ' . curl_error($ch)]); + exit; +} + +curl_close($ch); + +http_response_code($http_code); +echo $response_body; +?> diff --git a/dinos/auth.php b/dinos/auth.php index 840f793..812819e 100644 --- a/dinos/auth.php +++ b/dinos/auth.php @@ -5,6 +5,15 @@ * Führt den Google Login durch und speichert den Token. */ +$current_project = 'dinos'; +require_once __DIR__ . '/../auth.php'; + +// Nur Administratoren dürfen Google OAuth Konten verknüpfen +if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) { + header('Location: /'); + exit(); +} + require_once 'google_helper.php'; // Konfiguration diff --git a/dinos/index.php b/dinos/index.php index 0d44e53..ba49f6f 100644 --- a/dinos/index.php +++ b/dinos/index.php @@ -16,25 +16,6 @@ $dailyLimitEco = 100; // Eco (Flash) etwa 3-4 Cent pro Bild $dailyLimitPro = 10; // Pro (Imagen 4) etwa 3-4 Cent pro Bild $dailyLimitUltra = 10; // Ultra (Gemini 3) etwa 12 Cent pro Bild -// API-Key sicher aus .env laden -$apiKey = ''; -$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env']; -foreach ($envFiles as $envFile) { - if (file_exists($envFile)) { - foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { - $line = trim($line); - if ($line === '' || $line[0] === '#') continue; - if (strpos($line, '=') !== false) { - list($k, $v) = explode('=', $line, 2); - if (trim($k) === 'GEMINI_API_KEY') { - $apiKey = trim(trim($v), '"\''); - break 2; - } - } - } - } -} - // API Modelle (hier zentral ändern, wenn Versionen veralten) siehe apis.php $modelText = "gemini-2.5-flash"; // Für Fakten und Zufallsgenerator $modelImageEco = "gemini-2.5-flash-image"; // ECO Modus @@ -316,7 +297,6 @@ $totalUltra = $currentStats['total_ultra']; - - - - - - - - -
- - -
- -

- 🦕 - - Dino Wissenskarten - -

-

Generiere fotorealistische Bilder und entdecke spannende Fakten.

-
- - -
-
- - - - - -
- - - -
- - -
- - - -
- - - -
-
-
- - -
- - - - - - - - -
-
- 🦖 -
-

Tippe einen Dino-Namen ein und schau zu, was passiert.

-
-
- - - - - - - - - - \ No newline at end of file diff --git a/home/app/controllers/LoginController.php b/home/app/controllers/LoginController.php index d6837fa..449d692 100644 --- a/home/app/controllers/LoginController.php +++ b/home/app/controllers/LoginController.php @@ -20,9 +20,11 @@ class LoginController extends Controller { $userModel = $this->model('User'); $user = $userModel->authenticate($email, $password); if ($user) { + session_regenerate_id(true); $_SESSION['user_email'] = $email; $_SESSION['user_projects'] = $user['projects'] ?? []; $_SESSION['is_admin'] = $user['is_admin'] ?? false; + $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? ''; header('Location: ' . Config::get('BASE_URL', '/')); } else { Flash::set('Access denied: Invalid credentials.'); diff --git a/home/app/init.php b/home/app/init.php index a6c9648..281afb3 100644 --- a/home/app/init.php +++ b/home/app/init.php @@ -4,6 +4,7 @@ if (session_status() === PHP_SESSION_NONE) { session_set_cookie_params([ 'path' => '/', 'httponly' => true, + 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), 'samesite' => 'Lax' ]); session_start(); diff --git a/home/core/Controller.php b/home/core/Controller.php index 79f9fb3..8bb1db4 100644 --- a/home/core/Controller.php +++ b/home/core/Controller.php @@ -2,7 +2,7 @@ class Controller { - public function model($model) { + protected function model($model) { $modelPath = __DIR__ . '/../app/models/' . $model . '.php'; if (file_exists($modelPath)) { require_once $modelPath; @@ -11,7 +11,7 @@ class Controller { throw new Exception("Model '{$model}' existiert nicht."); } - public function view($view, $data = []) { + protected function view($view, $data = []) { if (!empty($data)) { extract($data); } @@ -25,20 +25,20 @@ class Controller { } } - public function json($data, $status = 200) { + protected function json($data, $status = 200) { http_response_code($status); header('Content-Type: application/json'); echo json_encode($data); exit; } - public function xml($data) { + protected function xml($data) { header('Content-Type: application/xml; charset=utf-8'); echo $data; exit; } - public function text($data) { + protected function text($data) { header('Content-Type: text/plain; charset=utf-8'); echo $data; exit; diff --git a/home/core/Router.php b/home/core/Router.php index c6f0f67..5628397 100644 --- a/home/core/Router.php +++ b/home/core/Router.php @@ -16,8 +16,9 @@ class Router { $this->controller = new $this->controller; // Method Check if (isset($url[1])) { - if (method_exists($this->controller, $url[1])) { - $this->method = $url[1]; + $candidateMethod = $url[1]; + if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) { + $this->method = $candidateMethod; unset($url[1]); } } diff --git a/home/core/Security.php b/home/core/Security.php index b23dcb6..7c01785 100644 --- a/home/core/Security.php +++ b/home/core/Security.php @@ -9,7 +9,7 @@ class Security { } // CSRF Validation public static function checkCsrf($token) { - if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) { + if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], (string)$token)) { throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu."); } return true; diff --git a/home/public/index.php b/home/public/index.php index 43dec3d..8410a65 100644 --- a/home/public/index.php +++ b/home/public/index.php @@ -1,11 +1,12 @@ '/', + 'httponly' => true, + 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), + 'samesite' => 'Lax' + ]); session_start(); } diff --git a/philcore/core/Controller.php b/philcore/core/Controller.php index f747ab4..c3e3a42 100644 --- a/philcore/core/Controller.php +++ b/philcore/core/Controller.php @@ -2,7 +2,7 @@ class Controller { // Modell laden - public function model($model) { + protected function model($model) { $modelPath = __DIR__ . '/../app/models/' . $model . '.php'; if (file_exists($modelPath)) { require_once $modelPath; @@ -12,7 +12,7 @@ class Controller { } // View laden - public function view($view, $data = []) { + protected function view($view, $data = []) { if (!empty($data)) { extract($data); } @@ -28,10 +28,22 @@ class Controller { } // JSON-Response für APIs und AJAX - public function json($data, $status = 200) { + protected function json($data, $status = 200) { http_response_code($status); header('Content-Type: application/json'); echo json_encode($data); exit; } + + protected function xml($data) { + header('Content-Type: application/xml; charset=utf-8'); + echo $data; + exit; + } + + protected function text($data) { + header('Content-Type: text/plain; charset=utf-8'); + echo $data; + exit; + } } \ No newline at end of file diff --git a/philcore/core/Router.php b/philcore/core/Router.php index 2050d09..da5f15c 100644 --- a/philcore/core/Router.php +++ b/philcore/core/Router.php @@ -21,8 +21,9 @@ class Router { // 2. Methode prüfen (z.B. /user/edit -> Methode edit()) if (isset($url[1])) { - if (method_exists($this->controller, $url[1])) { - $this->method = $url[1]; + $candidateMethod = $url[1]; + if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) { + $this->method = $candidateMethod; unset($url[1]); } } diff --git a/philcore/core/Security.php b/philcore/core/Security.php index bdf8548..7c4bfef 100644 --- a/philcore/core/Security.php +++ b/philcore/core/Security.php @@ -10,7 +10,7 @@ class Security { // Token beim POST-Request prüfen public static function checkCsrf($token) { - if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) { + if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], (string)$token)) { throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu."); } return true; diff --git a/plants/api.php b/plants/api.php index 0b8bf8e..e1c73dd 100644 --- a/plants/api.php +++ b/plants/api.php @@ -1,5 +1,7 @@ - - - - - Benachrichtigungs-Test - - - -
-

Test für Browser-Benachrichtigungen

-

Klicke auf den Knopf, um eine Test-Benachrichtigung auszulösen.

- -
- - - - diff --git a/plants/test_prompt.php b/plants/test_prompt.php deleted file mode 100644 index 3d386a5..0000000 --- a/plants/test_prompt.php +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - Prompt-Vergleich - - - - -

Prompt-Vergleich

-

Lade ein Bild hoch und vergleiche die Ergebnisse mit dem alten und dem neuen Prompt.

- -
- - -
- - - -
-
-

Alter Prompt

-
-
-
-

Finaler Profi-Prompt (V6)

-
-
-
- - - - - diff --git a/test/index.html b/test/index.html deleted file mode 100644 index 090d6b2..0000000 --- a/test/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - Test Projekt - - -

Zugriff gewährt!

-

Willkommen im Test-Projekt, .

- - \ No newline at end of file