Security: OAuth-state, Verschlüsselung, Session-Timeout & weitere Härtung
- m365_callback.php: OAuth-state-Validierung gegen Login-CSRF - includes/Crypto.php: Verschlüsselung-at-rest für UniFi-Passwörter (AES-256-GCM/libsodium) mit Klartext-Fallback für Bestandsinstallationen - install.php: APP_KEY-Generierung + Reinstall nur mit Admin-Session - Auth.php: absolutes Session-Timeout (SESSION_LIFETIME) durchsetzen - index.php: CSRF + Throttle auch für anonyme öffentliche Voucher-Erstellung - UniFiController.php: createVoucher liefert nicht mehr den falschen Code bei parallelen Erstellungen (note-Match statt blindes reset()) - display_errors in allen Entry-Points deaktiviert, log_errors aktiviert - test.php & m365_debug.php hinter requireAdmin() (Info-Leak) - m365_debug.php: abgeschnittene/kaputte Datei vervollständigt
This commit is contained in:
parent
bf3e55a967
commit
3483da274f
17 changed files with 306 additions and 36 deletions
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
|
|
@ -37,7 +38,7 @@ if (isset($_GET['ajax_stats'])) {
|
|||
$controller = new UniFiController(
|
||||
$site['unifi_controller_url'],
|
||||
$site['unifi_username'],
|
||||
$site['unifi_password'],
|
||||
Crypto::decrypt($site['unifi_password']),
|
||||
$site['site_id']
|
||||
);
|
||||
$controller->syncVouchersToDatabase($db, $site['id']);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
|
|
@ -44,7 +45,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_site'])) {
|
|||
// Mit neuem Passwort aktualisieren
|
||||
$db->execute(
|
||||
"UPDATE sites SET name = ?, site_id = ?, unifi_controller_url = ?, unifi_username = ?, unifi_password = ?, public_access = ? WHERE id = ?",
|
||||
[$name, $siteIdStr, $controllerUrl, $username, $password, $publicAccess, $siteId]
|
||||
[$name, $siteIdStr, $controllerUrl, $username, Crypto::encrypt($password), $publicAccess, $siteId]
|
||||
);
|
||||
} else {
|
||||
// Ohne Passwort-Änderung
|
||||
|
|
@ -88,7 +89,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_site'])) {
|
|||
$db->execute(
|
||||
"INSERT INTO sites (name, site_id, unifi_controller_url, unifi_username, unifi_password, public_access)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[$name, $siteId, $controllerUrl, $username, $password, $publicAccess]
|
||||
[$name, $siteId, $controllerUrl, $username, Crypto::encrypt($password), $publicAccess]
|
||||
);
|
||||
|
||||
$success = 'Site erfolgreich hinzugefügt!';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
|
|
@ -68,7 +69,7 @@ if (isset($_GET['ajax_get_vouchers']) && isset($_GET['site_id'])) {
|
|||
$controller = new UniFiController(
|
||||
$site['unifi_controller_url'],
|
||||
$site['unifi_username'],
|
||||
$site['unifi_password'],
|
||||
Crypto::decrypt($site['unifi_password']),
|
||||
$site['site_id']
|
||||
);
|
||||
$controller->syncVouchersToDatabase($db, $siteId);
|
||||
|
|
@ -149,7 +150,7 @@ if (isset($_POST['ajax_delete']) && isset($_POST['voucher_id']) && isset($_POST[
|
|||
$controller = new UniFiController(
|
||||
$site['unifi_controller_url'],
|
||||
$site['unifi_username'],
|
||||
$site['unifi_password'],
|
||||
Crypto::decrypt($site['unifi_password']),
|
||||
$site['site_id']
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ define('DB_NAME', '');
|
|||
define('DB_USER', '');
|
||||
define('DB_PASS', '');
|
||||
|
||||
// Anwendungs-Schluessel fuer Verschluesselung-at-rest (UniFi-Passwoerter).
|
||||
// Wird vom Installer automatisch mit einem zufaelligen Wert befuellt.
|
||||
// Leer = keine Verschluesselung (Klartext, Legacy-Verhalten).
|
||||
define('APP_KEY', '');
|
||||
|
||||
// Sitzungs-Einstellungen
|
||||
define('SESSION_LIFETIME', 3600); // 1 Stunde
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ try {
|
|||
$controller = new UniFiController(
|
||||
$site['unifi_controller_url'],
|
||||
$site['unifi_username'],
|
||||
$site['unifi_password'],
|
||||
Crypto::decrypt($site['unifi_password']),
|
||||
$site['site_id']
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,19 @@ class Auth {
|
|||
|
||||
// Prüfen ob eingeloggt
|
||||
public function isLoggedIn() {
|
||||
return isset($_SESSION['user_id']) && isset($_SESSION['login_time']);
|
||||
if (!isset($_SESSION['user_id']) || !isset($_SESSION['login_time'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Absolutes Session-Timeout durchsetzen (SESSION_LIFETIME aus config.php).
|
||||
// Bisher wurde die Lebensdauer nie geprueft – Sessions liefen unbegrenzt.
|
||||
$lifetime = defined('SESSION_LIFETIME') ? (int)SESSION_LIFETIME : 3600;
|
||||
if ($lifetime > 0 && (time() - (int)$_SESSION['login_time']) > $lifetime) {
|
||||
$this->logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfen ob Admin
|
||||
|
|
|
|||
117
includes/Crypto.php
Normal file
117
includes/Crypto.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
/**
|
||||
* Crypto - Symmetrische Verschluesselung fuer sensible Felder (z.B. UniFi-Passwoerter).
|
||||
*
|
||||
* Designziele:
|
||||
* - Verschluesselung-at-rest mit einem Schluessel (APP_KEY) aus der config.php.
|
||||
* - Vollstaendige Abwaertskompatibilitaet: Bestehende Installationen ohne APP_KEY
|
||||
* und bereits im Klartext gespeicherte Werte funktionieren unveraendert weiter.
|
||||
* decrypt() gibt Werte, die nicht unserem Ciphertext-Format entsprechen,
|
||||
* unveraendert zurueck (Klartext-Passthrough).
|
||||
* - encrypt() verschluesselt nur, wenn ein APP_KEY vorhanden ist – sonst Passthrough.
|
||||
*
|
||||
* Format des Ciphertexts: "enc:v1:" . base64(nonce|ciphertext)
|
||||
*/
|
||||
class Crypto {
|
||||
private const PREFIX = 'enc:v1:';
|
||||
|
||||
/** Liefert den 32-Byte-Schluessel oder null, wenn kein/ungueltiger APP_KEY gesetzt ist. */
|
||||
private static function key() {
|
||||
if (!defined('APP_KEY') || APP_KEY === '') {
|
||||
return null;
|
||||
}
|
||||
$key = base64_decode(APP_KEY, true);
|
||||
if ($key === false || strlen($key) !== 32) {
|
||||
return null;
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
/** Erzeugt einen neuen, base64-kodierten 32-Byte-Schluessel fuer die config.php. */
|
||||
public static function generateKey() {
|
||||
return base64_encode(random_bytes(32));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschluesselt einen Klartext. Ohne gueltigen APP_KEY wird der Wert
|
||||
* unveraendert zurueckgegeben (kein Bruch bestehender Installationen).
|
||||
*/
|
||||
public static function encrypt($plaintext) {
|
||||
if ($plaintext === null || $plaintext === '') {
|
||||
return $plaintext;
|
||||
}
|
||||
$key = self::key();
|
||||
if ($key === null) {
|
||||
return $plaintext; // Kein Schluessel -> Klartext (Legacy-Verhalten)
|
||||
}
|
||||
|
||||
// Bevorzugt libsodium (PHP-Core seit 7.2), sonst OpenSSL.
|
||||
if (function_exists('sodium_crypto_secretbox')) {
|
||||
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||
$cipher = sodium_crypto_secretbox($plaintext, $nonce, $key);
|
||||
return self::PREFIX . base64_encode($nonce . $cipher);
|
||||
}
|
||||
if (function_exists('openssl_encrypt')) {
|
||||
$ivLen = openssl_cipher_iv_length('aes-256-gcm');
|
||||
$iv = random_bytes($ivLen);
|
||||
$tag = '';
|
||||
$cipher = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
if ($cipher === false) {
|
||||
return $plaintext;
|
||||
}
|
||||
return self::PREFIX . base64_encode($iv . $tag . $cipher);
|
||||
}
|
||||
|
||||
// Keine Krypto-Funktion verfuegbar -> Klartext (besser als Datenverlust)
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entschluesselt einen Wert. Nicht-verschluesselte Werte (Legacy/Klartext)
|
||||
* werden unveraendert zurueckgegeben.
|
||||
*/
|
||||
public static function decrypt($value) {
|
||||
if ($value === null || $value === '' || strpos($value, self::PREFIX) !== 0) {
|
||||
return $value; // Klartext-Passthrough
|
||||
}
|
||||
$key = self::key();
|
||||
if ($key === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$raw = base64_decode(substr($value, strlen(self::PREFIX)), true);
|
||||
if ($raw === false) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (function_exists('sodium_crypto_secretbox_open')) {
|
||||
$nonceLen = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
|
||||
if (strlen($raw) <= $nonceLen) {
|
||||
return $value;
|
||||
}
|
||||
$nonce = substr($raw, 0, $nonceLen);
|
||||
$cipher = substr($raw, $nonceLen);
|
||||
$plain = sodium_crypto_secretbox_open($cipher, $nonce, $key);
|
||||
return $plain === false ? $value : $plain;
|
||||
}
|
||||
if (function_exists('openssl_decrypt')) {
|
||||
$ivLen = openssl_cipher_iv_length('aes-256-gcm');
|
||||
$tagLen = 16;
|
||||
if (strlen($raw) <= $ivLen + $tagLen) {
|
||||
return $value;
|
||||
}
|
||||
$iv = substr($raw, 0, $ivLen);
|
||||
$tag = substr($raw, $ivLen, $tagLen);
|
||||
$cipher = substr($raw, $ivLen + $tagLen);
|
||||
$plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
return $plain === false ? $value : $plain;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/** Prueft, ob ein Wert bereits in unserem verschluesselten Format vorliegt. */
|
||||
public static function isEncrypted($value) {
|
||||
return is_string($value) && strpos($value, self::PREFIX) === 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/Crypto.php';
|
||||
|
||||
class UniFiController {
|
||||
private $controllerUrl;
|
||||
private $username;
|
||||
|
|
@ -169,16 +171,44 @@ class UniFiController {
|
|||
throw new Exception("Voucher konnte nicht erstellt werden");
|
||||
}
|
||||
|
||||
// Voucher-Code abrufen
|
||||
// Voucher-Code abrufen. WICHTIG: getVouchers() liefert die Voucher
|
||||
// unsortiert zurueck – ein blindes reset() kann bei parallelen
|
||||
// Erstellungen den falschen (fremden) Code liefern. Daher gezielt
|
||||
// nach dem soeben erstellten Voucher suchen: gleiche note + neueste
|
||||
// create_time.
|
||||
$vouchers = $this->getVouchers();
|
||||
|
||||
|
||||
if (empty($vouchers)) {
|
||||
throw new Exception("Voucher-Code konnte nicht abgerufen werden");
|
||||
}
|
||||
|
||||
// Neuesten Voucher zurückgeben
|
||||
$latestVoucher = reset($vouchers);
|
||||
|
||||
|
||||
$latestVoucher = null;
|
||||
foreach ($vouchers as $voucher) {
|
||||
// Nur Voucher mit passender Notiz beruecksichtigen
|
||||
if (($voucher['note'] ?? null) !== $voucherName) {
|
||||
continue;
|
||||
}
|
||||
if ($latestVoucher === null
|
||||
|| ($voucher['create_time'] ?? 0) > ($latestVoucher['create_time'] ?? 0)) {
|
||||
$latestVoucher = $voucher;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: falls keine note-Uebereinstimmung (z.B. Sonderzeichen),
|
||||
// den global neuesten Voucher nehmen.
|
||||
if ($latestVoucher === null) {
|
||||
foreach ($vouchers as $voucher) {
|
||||
if ($latestVoucher === null
|
||||
|| ($voucher['create_time'] ?? 0) > ($latestVoucher['create_time'] ?? 0)) {
|
||||
$latestVoucher = $voucher;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($latestVoucher === null || empty($latestVoucher['code'])) {
|
||||
throw new Exception("Voucher-Code konnte nicht abgerufen werden");
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $latestVoucher['code'],
|
||||
'formatted_code' => $this->formatVoucherCode($latestVoucher['code']),
|
||||
|
|
|
|||
44
index.php
44
index.php
|
|
@ -1,7 +1,8 @@
|
|||
<?php
|
||||
// Error Reporting für Debugging
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
|
|
@ -13,6 +14,32 @@ $auth = new Auth();
|
|||
$db = Database::getInstance();
|
||||
$mailer = new Mailer();
|
||||
|
||||
/**
|
||||
* Session-basierter Throttle fuer die anonyme oeffentliche Voucher-Erstellung.
|
||||
* Erlaubt max. 10 Erstellungen in 10 Minuten pro Session. Verhindert, dass
|
||||
* der oeffentliche Modus zum Spammen des UniFi-Controllers missbraucht wird.
|
||||
*/
|
||||
function isVoucherRateLimited() {
|
||||
$window = 600; // 10 Minuten
|
||||
$maxRequests = 10;
|
||||
$now = time();
|
||||
|
||||
$timestamps = $_SESSION['voucher_create_times'] ?? [];
|
||||
// Nur Eintraege innerhalb des Zeitfensters behalten
|
||||
$timestamps = array_values(array_filter($timestamps, function ($t) use ($now, $window) {
|
||||
return ($now - $t) < $window;
|
||||
}));
|
||||
|
||||
if (count($timestamps) >= $maxRequests) {
|
||||
$_SESSION['voucher_create_times'] = $timestamps;
|
||||
return true;
|
||||
}
|
||||
|
||||
$timestamps[] = $now;
|
||||
$_SESSION['voucher_create_times'] = $timestamps;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Settings laden
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
|
|
@ -67,8 +94,14 @@ if ($auth->isLoggedIn()) {
|
|||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
|
||||
if (!$publicAccess && !$auth->isLoggedIn()) {
|
||||
$error = 'Sie müssen angemeldet sein';
|
||||
} elseif ($auth->isLoggedIn() && !$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
} elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
// CSRF wird jetzt fuer ALLE geprueft – auch fuer anonyme oeffentliche
|
||||
// Erstellung (Token wird per Session auch ohne Login vergeben).
|
||||
$error = 'Ungültiges Sicherheits-Token';
|
||||
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
|
||||
// Einfacher Session-basierter Throttle gegen Missbrauch/Spam im
|
||||
// oeffentlichen Modus (kein Login = kein Benutzerkontext).
|
||||
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
|
||||
} else {
|
||||
try {
|
||||
$siteId = (int)($_POST['site_id'] ?? 0);
|
||||
|
|
@ -114,7 +147,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
|
|||
$controller = new UniFiController(
|
||||
$site['unifi_controller_url'],
|
||||
$site['unifi_username'],
|
||||
$site['unifi_password'],
|
||||
Crypto::decrypt($site['unifi_password']),
|
||||
$site['site_id']
|
||||
);
|
||||
|
||||
|
|
@ -558,9 +591,8 @@ $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
|
|||
<!-- FIX: Trigger-Feld kommt nicht mehr vom Submit-Button -->
|
||||
<input type="hidden" name="create_voucher" value="1">
|
||||
|
||||
<?php if ($auth->isLoggedIn()): ?>
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<?php endif; ?>
|
||||
<!-- CSRF-Token fuer alle (auch anonyme oeffentliche Erstellung) -->
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($auth->getCsrfToken()) ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="voucher_name">Voucher-Name *</label>
|
||||
|
|
|
|||
29
install.php
29
install.php
|
|
@ -1,9 +1,27 @@
|
|||
<?php
|
||||
session_start();
|
||||
|
||||
// Prüfen ob bereits installiert
|
||||
if (file_exists(__DIR__ . '/config.php') && !isset($_GET['reinstall'])) {
|
||||
die('System bereits installiert. Wenn Sie neu installieren möchten, löschen Sie die config.php oder rufen Sie install.php?reinstall=1 auf.');
|
||||
// Prüfen ob bereits installiert.
|
||||
// Wenn eine config.php existiert, darf der Installer NICHT mehr ohne Weiteres
|
||||
// erreichbar sein – sonst koennte jeder die Konfiguration ueberschreiben und
|
||||
// einen neuen Admin anlegen. Reinstall ist nur fuer angemeldete Admins erlaubt.
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
if (!isset($_GET['reinstall'])) {
|
||||
die('System bereits installiert. Eine Neuinstallation ist nur fuer angemeldete Administratoren ueber install.php?reinstall=1 moeglich.');
|
||||
}
|
||||
|
||||
// Reinstall angefordert -> Admin-Authentifizierung erzwingen
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
try {
|
||||
$reinstallAuth = new Auth();
|
||||
if (!$reinstallAuth->isAdmin()) {
|
||||
die('Neuinstallation nicht erlaubt: Bitte zuerst als Administrator <a href="login.php">anmelden</a>.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
die('Neuinstallation nicht moeglich (Konfigurationsfehler).');
|
||||
}
|
||||
}
|
||||
|
||||
$step = isset($_POST['step']) ? (int)$_POST['step'] : 1;
|
||||
|
|
@ -130,6 +148,9 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
$configContent .= "define('DB_NAME', '{$db['name']}');\n";
|
||||
$configContent .= "define('DB_USER', '{$db['user']}');\n";
|
||||
$configContent .= "define('DB_PASS', '" . addslashes($db['pass']) . "');\n\n";
|
||||
$configContent .= "// Anwendungs-Schluessel fuer Verschluesselung-at-rest (z.B. UniFi-Passwoerter)\n";
|
||||
$configContent .= "// NICHT aendern, sonst koennen bestehende verschluesselte Werte nicht mehr gelesen werden.\n";
|
||||
$configContent .= "define('APP_KEY', '" . base64_encode(random_bytes(32)) . "');\n\n";
|
||||
$configContent .= "// Sitzungs-Einstellungen\n";
|
||||
$configContent .= "define('SESSION_LIFETIME', 3600); // 1 Stunde\n\n";
|
||||
$configContent .= "// Zeitzone\n";
|
||||
|
|
@ -140,7 +161,7 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
// .htaccess erstellen (ohne Rewrite Rules die Probleme machen)
|
||||
$htaccess = "# UniFi Voucher System\n\n";
|
||||
$htaccess .= "# Security\n";
|
||||
$htaccess .= "<FilesMatch \"(config\\.php|database\\.sql|install\\.php|test\\.php|\\.md)$\">\n";
|
||||
$htaccess .= "<FilesMatch \"(config\\.php|database\\.sql|install\\.php|test\\.php|m365_debug\\.php|\\.md)$\">\n";
|
||||
$htaccess .= " Order Allow,Deny\n";
|
||||
$htaccess .= " Deny from all\n";
|
||||
$htaccess .= "</FilesMatch>\n\n";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<?php
|
||||
// Error Reporting (kann nach erfolgreicher Einrichtung entfernt werden)
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
// Absolute Pfade verwenden
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<?php
|
||||
// Umfassendes Error Reporting
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
// Versuche Dateien zu laden
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
|
|
@ -36,8 +37,18 @@ if (isset($_GET['error'])) {
|
|||
|
||||
// Authorization Code erhalten
|
||||
if (isset($_GET['code'])) {
|
||||
// OAuth-State validieren (CSRF-Schutz). Der State wurde in login.php erzeugt
|
||||
// und in der Session hinterlegt; er muss exakt zurueckkommen.
|
||||
$sessionState = $_SESSION['m365_state'] ?? '';
|
||||
$returnedState = $_GET['state'] ?? '';
|
||||
unset($_SESSION['m365_state']); // One-Time-Token, nach Pruefung verbrauchen
|
||||
|
||||
if ($sessionState === '' || !hash_equals($sessionState, $returnedState)) {
|
||||
die("Ungültiger oder fehlender Sicherheits-Token (OAuth state). Bitte erneut anmelden.<br><a href='login.php'>Zurück zum Login</a>");
|
||||
}
|
||||
|
||||
$code = $_GET['code'];
|
||||
|
||||
|
||||
// Token anfordern
|
||||
$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
|
||||
// Diagnose-Seite nur fuer angemeldete Admins (leakt sonst M365-Konfiguration)
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
|
|
@ -78,4 +84,22 @@ $redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
|
|||
<h2>1. Konfiguration Status</h2>
|
||||
<p>Client ID: <?= !empty($clientId) ? '<span class="ok">✓ Gesetzt</span>' : '<span class="error">✗ Fehlt</span>' ?></p>
|
||||
<p>Client Secret: <?= !empty($clientSecret) ? '<span class="ok">✓ Gesetzt</span>' : '<span class="error">✗ Fehlt</span>' ?></p>
|
||||
<p>Tenant ID: <?= !empty($tenant
|
||||
<p>Tenant ID: <?= !empty($tenantId) ? '<span class="ok">✓ Gesetzt</span>' : '<span class="error">✗ Fehlt</span>' ?></p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Redirect URI</h2>
|
||||
<p>Diese URI muss exakt in der Azure-App-Registrierung hinterlegt sein:</p>
|
||||
<div class="url"><?= htmlspecialchars($redirectUri) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>3. Hinweise</h2>
|
||||
<ul>
|
||||
<li>Alle drei Werte (Client ID, Client Secret, Tenant ID) müssen gesetzt sein.</li>
|
||||
<li>Die Redirect URI in Azure AD muss exakt mit der obigen übereinstimmen.</li>
|
||||
<li>Benötigte API-Berechtigungen: <code>openid</code>, <code>profile</code>, <code>email</code>, <code>User.Read</code>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
15
test.php
15
test.php
|
|
@ -1,6 +1,17 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
// Diagnose-Seite nur fuer angemeldete Admins zugaenglich (verhindert Info-Leak)
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/Crypto.php';
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
}
|
||||
|
||||
echo "<h1>System Test</h1>";
|
||||
|
||||
|
|
@ -111,7 +122,7 @@ try {
|
|||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'username' => $site['unifi_username'],
|
||||
'password' => $site['unifi_password']
|
||||
'password' => Crypto::decrypt($site['unifi_password'])
|
||||
]),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue