Enhance API functionality and security: Added rate limiting and domain validation across multiple API endpoints, improved error handling for missing or invalid parameters, and refactored email handling in contact form for better security and maintainability. Updated README.md with production build instructions and prerequisites.

This commit is contained in:
smueller
2026-05-22 14:50:20 +02:00
parent 5f5be4a4cb
commit ebf6f82bb6
21 changed files with 1007 additions and 355 deletions

8
backend/.htaccess Normal file
View File

@@ -0,0 +1,8 @@
# Direkten Zugriff auf Backend-Dateien verhindern (Document Root = public/)
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>

View File

@@ -1,12 +1,14 @@
<?php
/**
* HexaDNS - DNS Lookup API
*
*
* Führt echte DNS-Abfragen durch und gibt die Ergebnisse als JSON zurück.
*
*
* Verwendung: GET /api/dns-lookup.php?domain=example.com
*/
require_once __DIR__ . '/../includes/api-helpers.php';
// CORS Headers für Frontend-Zugriff
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
@@ -19,26 +21,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
// Nur GET-Anfragen erlauben
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['error' => 'Nur GET-Anfragen erlaubt']);
exit;
}
// Domain-Parameter prüfen
$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
if (empty($domain)) {
http_response_code(400);
echo json_encode(['error' => 'Domain-Parameter fehlt']);
exit;
if (!checkApiRateLimit('dns-lookup')) {
rejectApiRateLimit();
}
// Domain validieren (einfache Prüfung)
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
$domain = getValidatedDomainParam();
if ($domain === null) {
http_response_code(400);
echo json_encode(['error' => 'Ungültiges Domain-Format']);
echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}

View File

@@ -1,12 +1,14 @@
<?php
/**
* HexaDNS - DNS Propagation Check API
*
*
* Prüft DNS-Records bei verschiedenen öffentlichen DNS-Servern
*
*
* Verwendung: GET /api/dns-propagation.php?domain=example.com&type=A
*/
require_once __DIR__ . '/../includes/api-helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
@@ -17,6 +19,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
if (!checkApiRateLimit('dns-propagation')) {
rejectApiRateLimit();
}
// Öffentliche DNS-Server für Propagation-Check
$dnsServers = [
['name' => 'Google', 'ip' => '8.8.8.8', 'location' => 'Global'],
@@ -29,18 +35,12 @@ $dnsServers = [
['name' => 'Level3', 'ip' => '4.2.2.1', 'location' => 'USA'],
];
$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
$domain = getValidatedDomainParam();
$type = isset($_GET['type']) ? strtoupper(trim($_GET['type'])) : 'A';
if (empty($domain)) {
if ($domain === null) {
http_response_code(400);
echo json_encode(['error' => 'Domain-Parameter fehlt']);
exit;
}
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
http_response_code(400);
echo json_encode(['error' => 'Ungültiges Domain-Format']);
echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}
@@ -100,7 +100,12 @@ function queryDnsServer(string $domain, string $type, string $server): array {
$records = [];
// Versuche zuerst dig zu verwenden (genauer)
$digResult = @shell_exec("dig @{$server} {$domain} {$type} +short +time=2 +tries=1 2>/dev/null");
$digResult = @shell_exec(
'dig @' . escapeshellarg($server) . ' '
. escapeshellarg($domain) . ' '
. escapeshellarg($type)
. ' +short +time=2 +tries=1 2>/dev/null'
);
if ($digResult !== null && trim($digResult) !== '') {
$lines = array_filter(explode("\n", trim($digResult)));

View File

@@ -7,6 +7,8 @@
* Verwendung: GET /api/ping-check.php?domain=example.com
*/
require_once __DIR__ . '/../includes/api-helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
@@ -17,21 +19,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
if (empty($domain)) {
http_response_code(400);
echo json_encode(['error' => 'Domain-Parameter fehlt']);
exit;
if (!checkApiRateLimit('ping-check')) {
rejectApiRateLimit();
}
// Protokoll und Pfad entfernen
$domain = preg_replace('/^(https?:\/\/)?/', '', $domain);
$domain = explode('/', $domain)[0];
$domain = getValidatedDomainParam();
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
if ($domain === null) {
http_response_code(400);
echo json_encode(['error' => 'Ungültiges Domain-Format']);
echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}
@@ -99,7 +95,8 @@ function checkIcmpPing(string $domain): array {
];
// Versuche ping-Kommando
$pingResult = @shell_exec("ping -c 3 -W 2 {$domain} 2>/dev/null");
$safeDomain = escapeshellarg($domain);
$pingResult = @shell_exec("ping -c 3 -W 2 {$safeDomain} 2>/dev/null");
if ($pingResult) {
// Prüfe auf erfolgreiche Antworten

View File

@@ -1,12 +1,14 @@
<?php
/**
* HexaDNS - Reverse DNS Lookup API
*
*
* Löst eine IP-Adresse zu einem Hostnamen auf
*
*
* Verwendung: GET /api/reverse-dns.php?ip=8.8.8.8
*/
require_once __DIR__ . '/../includes/api-helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
@@ -17,6 +19,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
if (!checkApiRateLimit('reverse-dns')) {
rejectApiRateLimit();
}
$ip = isset($_GET['ip']) ? trim($_GET['ip']) : '';
if (empty($ip)) {

View File

@@ -1,12 +1,14 @@
<?php
/**
* HexaDNS - SSL Certificate Check API
*
*
* Prüft SSL-Zertifikat-Informationen einer Domain
*
*
* Verwendung: GET /api/ssl-check.php?domain=example.com
*/
require_once __DIR__ . '/../includes/api-helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
@@ -17,22 +19,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
if (empty($domain)) {
http_response_code(400);
echo json_encode(['error' => 'Domain-Parameter fehlt']);
exit;
if (!checkApiRateLimit('ssl-check')) {
rejectApiRateLimit();
}
// Protokoll und Pfad entfernen
$domain = preg_replace('/^(https?:\/\/)?/', '', $domain);
$domain = explode('/', $domain)[0];
$domain = explode(':', $domain)[0]; // Port entfernen
$domain = getValidatedDomainParam();
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
if ($domain === null) {
http_response_code(400);
echo json_encode(['error' => 'Ungültiges Domain-Format']);
echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}

View File

@@ -1,12 +1,14 @@
<?php
/**
* HexaDNS - WHOIS Lookup API
*
*
* Ruft WHOIS-Informationen für eine Domain ab
*
*
* Verwendung: GET /api/whois-lookup.php?domain=example.com
*/
require_once __DIR__ . '/../includes/api-helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
@@ -17,7 +19,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
if (!checkApiRateLimit('whois-lookup')) {
rejectApiRateLimit();
}
$domain = isset($_GET['domain']) ? trim((string) $_GET['domain']) : '';
if (empty($domain)) {
http_response_code(400);

View File

@@ -0,0 +1,64 @@
<?php
/**
* Zentrale Betreff-Konfiguration für das Kontaktformular
*/
/**
* @return array<string, string> Betreff-Schlüssel => Anzeigename
*/
function getContactSubjectMap(): array {
return [
'allgemeine-anfrage' => 'Allgemeine Anfrage',
'vpc-anfrage' => 'Virtual Private Container Anfrage',
'vps-anfrage' => 'Virtual Private Server Anfrage',
'mail-gateway-anfrage' => 'Mail Gateway Anfrage',
'webhosting-anfrage' => 'Webhosting Anfrage',
'it-beratung' => 'IT-Beratung',
'it-support' => 'IT-Support & Fehlerbehebung',
'netzwerk-wlan' => 'Netzwerk & WLAN-Einrichtung',
'it-sicherheit-backup' => 'IT-Sicherheit & Backup',
'webseiten-hosting-service' => 'Webseiten- & Hosting-Service',
'wartung-betreuung' => 'Wartung & Betreuung',
'support' => 'Technischer Support',
'beratung' => 'Persönliche Beratung',
'migration' => 'Migration/Umzug',
'sonstiges' => 'Sonstige Anfrage',
];
}
/**
* @param string $subjectKey
*/
function isAllowedContactSubject(string $subjectKey): bool {
return array_key_exists($subjectKey, getContactSubjectMap());
}
/**
* Betreff aus ?product= oder ?package= für die Kontaktseite ableiten
*/
function getPreselectedContactSubject(): string {
$productMap = [
'vpc' => 'vpc-anfrage',
'vps' => 'vps-anfrage',
'mail-gateway' => 'mail-gateway-anfrage',
'webhosting' => 'webhosting-anfrage',
];
if (!empty($_GET['product'])) {
$product = strtolower(preg_replace('/[^a-z0-9-]/', '', (string) $_GET['product']));
if (isset($productMap[$product])) {
return $productMap[$product];
}
}
if (!empty($_GET['package'])) {
$package = strtolower(preg_replace('/[^a-z0-9-]/', '', (string) $_GET['package']));
foreach ($productMap as $productId => $subjectKey) {
if (str_starts_with($package, $productId . '-')) {
return $subjectKey;
}
}
}
return '';
}

View File

@@ -140,24 +140,6 @@ function isValidEmail($email) {
return true;
}
// CSRF Token generieren (wird in functions.php verwendet)
// Hinweis: Diese Funktion existiert auch in functions.php - hier nur als Fallback
if (!function_exists('generateCSRFToken')) {
function generateCSRFToken() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
}
// CSRF Token validieren
if (!function_exists('validateCSRFToken')) {
function validateCSRFToken($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
}
/**
* Hilfsfunktion zum Abrufen der Konfiguration als Array
* Kompatibilität mit contact-handler.php
@@ -183,6 +165,9 @@ function getHexaHostConfig($key = null) {
// Sicherheit
'max_requests_per_hour' => MAX_REQUESTS_PER_HOUR,
'honeypot_field' => 'website',
'enable_csrf' => ENABLE_CSRF_PROTECTION,
'min_message_length' => MIN_MESSAGE_LENGTH,
'max_message_length' => MAX_MESSAGE_LENGTH,
// Debug
'debug_mode' => DEBUG_MODE,

View File

@@ -0,0 +1,112 @@
<?php
/**
* Gemeinsame Hilfsfunktionen für öffentliche DNS/API-Endpunkte
*/
/**
* Client-IP für Rate-Limiting (Cloudflare-sicher, kein blindes X-Forwarded-For)
*/
function getApiClientIp(): string {
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])
&& filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$isTrustedProxy = filter_var(
$remoteAddr,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) === false;
if ($isTrustedProxy) {
foreach (['HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $header) {
if (empty($_SERVER[$header])) {
continue;
}
$ip = trim(explode(',', $_SERVER[$header])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return $remoteAddr;
}
/**
* Einfaches Rate-Limiting pro Endpunkt und IP
*/
function checkApiRateLimit(string $endpoint, int $maxPerHour = 120): bool {
$ip = getApiClientIp();
$cacheFile = sys_get_temp_dir() . '/hexahost_api_' . md5($endpoint . '_' . $ip) . '.txt';
$currentTime = time();
$data = ['requests' => []];
$handle = @fopen($cacheFile, 'c+');
if ($handle === false) {
return true;
}
try {
if (!flock($handle, LOCK_EX)) {
return true;
}
$contents = stream_get_contents($handle);
if ($contents !== false && $contents !== '') {
$decoded = json_decode($contents, true);
if (is_array($decoded) && isset($decoded['requests'])) {
$data = $decoded;
}
}
$data['requests'] = array_values(array_filter(
$data['requests'],
static fn($timestamp) => ($currentTime - (int) $timestamp) < 3600
));
if (count($data['requests']) >= $maxPerHour) {
return false;
}
$data['requests'][] = $currentTime;
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, json_encode($data));
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
return true;
}
/**
* Domain aus GET-Parameter normalisieren und validieren
*/
function getValidatedDomainParam(string $param = 'domain'): ?string {
if (empty($_GET[$param])) {
return null;
}
$domain = trim((string) $_GET[$param]);
$domain = preg_replace('/^(https?:\/\/)?/', '', $domain);
$domain = explode('/', $domain)[0];
$domain = explode(':', $domain)[0];
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
return null;
}
return $domain;
}
/**
* Rate-Limit-JSON-Antwort senden und beenden
*/
function rejectApiRateLimit(): void {
http_response_code(429);
echo json_encode(['error' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.']);
exit;
}

View File

@@ -160,7 +160,7 @@
<script src="/assets/js/cookie-consent.js" defer></script>
<?php if (isset($additional_scripts)): ?>
<?php foreach ($additional_scripts as $script): ?>
<script src="<?php echo $script; ?>" defer></script>
<script src="<?php echo htmlspecialchars($script, ENT_QUOTES, 'UTF-8'); ?>" defer></script>
<?php endforeach; ?>
<?php endif; ?>
</body>

View File

@@ -95,4 +95,56 @@ function generateCSRFToken() {
}
return $_SESSION['csrf_token'];
}
/**
* CSRF-Token prüfen und nach Erfolg invalidieren (Replay-Schutz)
*/
function validateCSRFToken($token) {
if (!isset($_SESSION['csrf_token']) || !is_string($token)) {
return false;
}
if (!hash_equals($_SESSION['csrf_token'], $token)) {
return false;
}
unset($_SESSION['csrf_token']);
return true;
}
/**
* Werte für E-Mail-Header bereinigen (Header-Injection verhindern)
*/
function sanitizeHeaderValue(string $value): string {
return str_replace(["\r", "\n", "\0"], '', trim($value));
}
/**
* Client-IP für Logging (Cloudflare / vertrauenswürdiger Reverse-Proxy)
*/
function getClientIP(): string {
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])
&& filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$isTrustedProxy = filter_var(
$remoteAddr,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) === false;
if ($isTrustedProxy) {
foreach (['HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $header) {
if (empty($_SERVER[$header])) {
continue;
}
$ip = trim(explode(',', $_SERVER[$header])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return $remoteAddr;
}
?>