diff --git a/endpoints/ai/fetch_models.php b/endpoints/ai/fetch_models.php index 29bf9bd..1bbe72d 100644 --- a/endpoints/ai/fetch_models.php +++ b/endpoints/ai/fetch_models.php @@ -2,6 +2,7 @@ require_once '../../includes/connect_endpoint.php'; require_once '../../includes/validate_endpoint.php'; +require_once '../../includes/ssrf_helper.php'; $chatgptModelsApiUrl = 'https://api.openai.com/v1/models'; $geminiModelsApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models'; @@ -59,6 +60,21 @@ if ($aiType === 'chatgpt') { echo json_encode($response); exit; } + + // Scheme check + $parsedUrl = parse_url($aiOllamaHost); + if ( + !isset($parsedUrl['scheme']) || + !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) || + !filter_var($aiOllamaHost, FILTER_VALIDATE_URL) + ) { + echo json_encode(["success" => false, "message" => translate('invalid_host', $i18n)]); + exit; + } + + // SSRF check — dies automatically if private IP not in allowlist + $ssrf = validate_webhook_url_for_ssrf($aiOllamaHost, $db, $i18n); + $apiUrl = $aiOllamaHost . '/api/tags'; } // Initialize cURL diff --git a/endpoints/ai/generate_recommendations.php b/endpoints/ai/generate_recommendations.php index 3ed511c..2cfbb37 100644 --- a/endpoints/ai/generate_recommendations.php +++ b/endpoints/ai/generate_recommendations.php @@ -2,6 +2,7 @@ set_time_limit(300); require_once '../../includes/connect_endpoint.php'; require_once '../../includes/validate_endpoint.php'; +require_once '../../includes/ssrf_helper.php'; function getPricePerMonth($cycle, $frequency, $price) { @@ -80,7 +81,19 @@ if ($type == 'ollama') { echo json_encode($response); exit; } + $parsedUrl = parse_url($host); + if ( + !isset($parsedUrl['scheme']) || + !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) || + !filter_var($host, FILTER_VALIDATE_URL) + ) { + echo json_encode(["success" => false, "message" => translate('invalid_host', $i18n)]); + exit; + } + + $ssrf = validate_webhook_url_for_ssrf($host, $db, $i18n); } else { + $ssrf = null; $apiKey = isset($aiSettings['api_key']) ? $aiSettings['api_key'] : ''; if (empty($apiKey)) { $response = [ @@ -216,6 +229,7 @@ $ch = curl_init(); if ($type === 'ollama') { curl_setopt($ch, CURLOPT_URL, $host . '/api/generate'); + curl_setopt($ch, CURLOPT_RESOLVE, ["{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}"]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['model' => $model, 'prompt' => $prompt, 'stream' => false])); } else { diff --git a/endpoints/ai/save_settings.php b/endpoints/ai/save_settings.php index 99d34cf..27d1aa8 100644 --- a/endpoints/ai/save_settings.php +++ b/endpoints/ai/save_settings.php @@ -1,6 +1,7 @@ false, "message" => translate('invalid_host', $i18n)]); + exit; + } + + // SSRF check — dies automatically if private IP not in allowlist + validate_webhook_url_for_ssrf($aiOllamaHost, $db, $i18n); } else { $aiOllamaHost = ''; // Clear Ollama host if not using Ollama } diff --git a/endpoints/cronjobs/sendcancellationnotifications.php b/endpoints/cronjobs/sendcancellationnotifications.php index 32f5f0e..27de8fd 100644 --- a/endpoints/cronjobs/sendcancellationnotifications.php +++ b/endpoints/cronjobs/sendcancellationnotifications.php @@ -5,6 +5,7 @@ use PHPMailer\PHPMailer\Exception; require_once 'validate.php'; require_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php'; +require_once __DIR__ . '/../../includes/ssrf_helper.php'; require __DIR__ . '/../../libs/PHPMailer/PHPMailer.php'; require __DIR__ . '/../../libs/PHPMailer/SMTP.php'; @@ -273,107 +274,117 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) { // Discord notifications if enabled if ($discordNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); + $ssrf = is_url_safe_for_ssrf($discord['webhook_url'], $db); + if (!$ssrf) { + echo "Discord notification skipped: URL failed SSRF validation.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); - $title = translate('wallos_notification', $i18n); + $title = translate('wallos_notification', $i18n); - if ($user['name']) { - $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; - } else { - $message = "The following subscriptions are up for cancellation:\n"; - } + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } - foreach ($perUser as $subscription) { - $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; - } + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } - $postfields = [ - 'content' => $message - ]; + $postfields = [ + 'content' => $message + ]; - if (!empty($discord['bot_username'])) { - $postfields['username'] = $discord['bot_username']; - } + if (!empty($discord['bot_username'])) { + $postfields['username'] = $discord['bot_username']; + } - if (!empty($discord['bot_avatar_url'])) { - $postfields['avatar_url'] = $discord['bot_avatar_url']; - } + if (!empty($discord['bot_avatar_url'])) { + $postfields['avatar_url'] = $discord['bot_avatar_url']; + } - $ch = curl_init(); + $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields)); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json' - ]); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json' + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($ch); - curl_close($ch); + $response = curl_exec($ch); + curl_close($ch); - if ($result === false) { - echo "Error sending notifications: " . curl_error($ch) . "
"; - } else { - echo "Discord Notifications sent
"; + if ($response === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Discord Notifications sent
"; + } } } } // Gotify notifications if enabled if ($gotifyNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); + $ssrf = is_url_safe_for_ssrf($gotify['serverUrl'], $db); + if (!$ssrf) { + echo "Gotify notification skipped: URL failed SSRF validation.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); - if ($user['name']) { - $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; - } else { - $message = "The following subscriptions are up for cancellation:\n"; - } + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } - foreach ($perUser as $subscription) { - $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; - } + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } - $data = array( - 'message' => $message, - 'priority' => 5 - ); + $data = array( + 'message' => $message, + 'priority' => 5 + ); - $data_string = json_encode($data); + $data_string = json_encode($data); - $ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - array( - 'Content-Type: application/json', - 'Content-Length: ' . strlen($data_string) - ) - ); + $ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data_string) + ) + ); - if ($gotify['ignore_ssl']) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - } + if ($gotify['ignore_ssl']) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } - $result = curl_exec($ch); - if ($result === false) { - echo "Error sending notifications: " . curl_error($ch) . "
"; - } else { - echo "Gotify Notifications sent
"; + $result = curl_exec($ch); + if ($result === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Gotify Notifications sent
"; + } } } } @@ -469,112 +480,122 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) { // Ntfy notifications if enabled if ($ntfyNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); + $ssrf = is_url_safe_for_ssrf($ntfy['host'], $db); + if (!$ssrf) { + echo "Ntfy notification skipped: URL failed SSRF validation.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); - if ($user['name']) { - $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; - } else { - $message = "The following subscriptions are up for cancellation:\n"; - } + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } - foreach ($perUser as $subscription) { - $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; - } + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } - $headers = json_decode($ntfy["headers"], true); - $customheaders = array_map(function ($key, $value) { - return "$key: $value"; - }, array_keys($headers), $headers); + $headers = json_decode($ntfy["headers"], true); + $customheaders = array_map(function ($key, $value) { + return "$key: $value"; + }, array_keys($headers), $headers); - $ch = curl_init(); + $ch = curl_init(); - $ntfyHost = rtrim($ntfy["host"], '/'); - $ntfyTopic = $ntfy['topic']; + $ntfyHost = rtrim($ntfy["host"], '/'); + $ntfyTopic = $ntfy['topic']; - curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $message); - curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - if ($ntfy['ignore_ssl']) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - } + if ($ntfy['ignore_ssl']) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } - $response = curl_exec($ch); - curl_close($ch); + $response = curl_exec($ch); + curl_close($ch); - if ($response === false) { - echo "Error sending notifications: " . curl_error($ch) . "
"; - } else { - echo "Ntfy Notifications sent
"; + if ($response === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Ntfy Notifications sent
"; + } } } } // Webhook notifications if enabled if ($webhookNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); - - if ($user['name']) { - $payer = $user['name']; - } - - foreach ($perUser as $subscription) { - // Ensure the payload is reset for each subscription - $payload = $webhook['cancelation_payload']; - $payload = str_replace("{{subscription_name}}", $subscription['name'], $payload); - $payload = str_replace("{{subscription_price}}", $subscription['price'], $payload); - $payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload); - $payload = str_replace("{{subscription_category}}", $subscription['category'], $payload); - $payload = str_replace("{{subscription_payer}}", $payer, $payload); - $payload = str_replace("{{subscription_date}}", $subscription['date'], $payload); - $payload = str_replace("{{subscription_url}}", $subscription['url'], $payload); - $payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload); - - // Initialize cURL for each subscription - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $webhook['url']); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - - // Add headers if they exist - if (!empty($webhook['headers'])) { - $customheaders = preg_split("/\r\n|\n|\r/", $webhook['headers']); - curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + $ssrf = is_url_safe_for_ssrf($webhook['url'], $db); + if (!$ssrf) { + echo "Webhook notification skipped: URL failed SSRF validation.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if ($user['name']) { + $payer = $user['name']; } - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - // Handle SSL settings - if ($webhook['ignore_ssl']) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + + foreach ($perUser as $subscription) { + // Ensure the payload is reset for each subscription + $payload = $webhook['cancelation_payload']; + $payload = str_replace("{{subscription_name}}", $subscription['name'], $payload); + $payload = str_replace("{{subscription_price}}", $subscription['price'], $payload); + $payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload); + $payload = str_replace("{{subscription_category}}", $subscription['category'], $payload); + $payload = str_replace("{{subscription_payer}}", $payer, $payload); + $payload = str_replace("{{subscription_date}}", $subscription['date'], $payload); + $payload = str_replace("{{subscription_url}}", $subscription['url'], $payload); + $payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload); + + // Initialize cURL for each subscription + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $webhook['url']); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + + // Add headers if they exist + if (!empty($webhook['headers'])) { + $customheaders = preg_split("/\r\n|\n|\r/", $webhook['headers']); + curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + } + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Handle SSL settings + if ($webhook['ignore_ssl']) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } + + // Execute the cURL request + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($response === false || $httpCode >= 400) { + echo "Error sending cancellation notifications: " . curl_error($ch) . "
"; + } else { + echo "Webhook Cancellation Notification sent for subscription: " . $subscription['name'] . "
"; + } + + usleep(1000000); // 1s delay between requests } - - // Execute the cURL request - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($response === false || $httpCode >= 400) { - echo "Error sending cancellation notifications: " . curl_error($ch) . "
"; - } else { - echo "Webhook Cancellation Notification sent for subscription: " . $subscription['name'] . "
"; - } - - usleep(1000000); // 1s delay between requests } } } diff --git a/endpoints/cronjobs/sendnotifications.php b/endpoints/cronjobs/sendnotifications.php index 0b61dc1..0614eb2 100644 --- a/endpoints/cronjobs/sendnotifications.php +++ b/endpoints/cronjobs/sendnotifications.php @@ -5,6 +5,7 @@ use PHPMailer\PHPMailer\Exception; require_once 'validate.php'; require_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php'; +require_once __DIR__ . '/../../includes/ssrf_helper.php'; require __DIR__ . '/../../libs/PHPMailer/PHPMailer.php'; require __DIR__ . '/../../libs/PHPMailer/SMTP.php'; @@ -376,109 +377,119 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) { // Discord notifications if enabled if ($discordNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); + $ssrf = is_url_safe_for_ssrf($discord['webhook_url'], $db); + if (!$ssrf) { + echo "SSRF attempt detected for Discord webhook URL. Notifications not sent.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); - $title = translate('wallos_notification', $i18n); + $title = translate('wallos_notification', $i18n); - if ($user['name']) { - $message = $user['name'] . ", the following subscriptions are up for renewal:\n"; - } else { - $message = "The following subscriptions are up for renewal:\n"; - } + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for renewal:\n"; + } else { + $message = "The following subscriptions are up for renewal:\n"; + } - foreach ($perUser as $subscription) { - $dayText = getDaysText($subscription['days']); - $message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; - } + foreach ($perUser as $subscription) { + $dayText = getDaysText($subscription['days']); + $message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; + } - $postfields = [ - 'content' => $message - ]; + $postfields = [ + 'content' => $message + ]; - if (!empty($discord['bot_username'])) { - $postfields['username'] = $discord['bot_username']; - } + if (!empty($discord['bot_username'])) { + $postfields['username'] = $discord['bot_username']; + } - if (!empty($discord['bot_avatar_url'])) { - $postfields['avatar_url'] = $discord['bot_avatar_url']; - } + if (!empty($discord['bot_avatar_url'])) { + $postfields['avatar_url'] = $discord['bot_avatar_url']; + } - $ch = curl_init(); + $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields)); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json' - ]); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json' + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($ch); - curl_close($ch); + $response = curl_exec($ch); + curl_close($ch); - if ($result === false) { - echo "Error sending notifications: " . curl_error($ch) . "
"; - } else { - echo "Discord Notifications sent
"; + if ($result === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Discord Notifications sent
"; + } } } } // Gotify notifications if enabled if ($gotifyNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); + $ssrf = is_url_safe_for_ssrf($gotify['serverUrl'], $db); + if (!$ssrf) { + echo "SSRF attempt detected for Gotify server URL. Notifications not sent.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); - if ($user['name']) { - $message = $user['name'] . ", the following subscriptions are up for renewal:\n"; - } else { - $message = "The following subscriptions are up for renewal:\n"; - } + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for renewal:\n"; + } else { + $message = "The following subscriptions are up for renewal:\n"; + } - foreach ($perUser as $subscription) { - $dayText = getDaysText($subscription['days']); - $message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; - } + foreach ($perUser as $subscription) { + $dayText = getDaysText($subscription['days']); + $message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; + } - $data = array( - 'message' => $message, - 'priority' => 5 - ); + $data = array( + 'message' => $message, + 'priority' => 5 + ); - $data_string = json_encode($data); + $data_string = json_encode($data); - $ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - array( - 'Content-Type: application/json', - 'Content-Length: ' . strlen($data_string) - ) - ); + $ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data_string) + ) + ); - if ($gotify['ignore_ssl']) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - } + if ($gotify['ignore_ssl']) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } - $result = curl_exec($ch); - if ($result === false) { - echo "Error sending notifications: " . curl_error($ch) . "
"; - } else { - echo "Gotify Notifications sent
"; + $result = curl_exec($ch); + if ($result === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Gotify Notifications sent
"; + } } } } @@ -597,64 +608,69 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) { // Mattermost notifications if enabled if ($mattermostNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); + $ssrf = is_url_safe_for_ssrf($mattermost['webhook_url'], $db); + if (!$ssrf) { + echo "SSRF attempt detected for Mattermost webhook URL. Notifications not sent.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); - // Build Message Content - $messageContent = ""; - if ($user['name']) { - $messageContent = $user['name'] . ", the following subscriptions are up for renewal:\n"; - } else { - $messageContent = "The following subscriptions are up for renewal:\n"; - } - - foreach ($perUser as $subscription) { - $dayText = getDaysText($subscription['days']); - $messageContent .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; - } - - // Prepare Mattermost Data - $webhook_url = $mattermost['webhook_url']; - $data = array( - 'username' => $mattermost['bot_username'], - 'icon_emoji' => $mattermost['bot_icon_emoji'], - 'text' => mb_convert_encoding($messageContent, 'UTF-8', 'auto'), - ); - - $data_string = json_encode($data); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $webhook_url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - array( - 'Content-Type: application/json' - ), - ); - - $result = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - if ($result === false) { - echo "Error sending Mattermost notifications: " . curl_error($ch) . "
"; - } else { - $resultData = json_decode($result, true); - if (isset($resultData['code']) && $resultData['code'] == 200) { - echo "Mattermost Notifications sent successfully
"; + // Build Message Content + $messageContent = ""; + if ($user['name']) { + $messageContent = $user['name'] . ", the following subscriptions are up for renewal:\n"; } else { - $errorMsg = isset($resultData['msg']) ? $resultData['msg'] : 'Unknown error'; - echo "Mattermost API error: " . $errorMsg . "
"; + $messageContent = "The following subscriptions are up for renewal:\n"; } + + foreach ($perUser as $subscription) { + $dayText = getDaysText($subscription['days']); + $messageContent .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; + } + + // Prepare Mattermost Data + $webhook_url = $mattermost['webhook_url']; + $data = array( + 'username' => $mattermost['bot_username'], + 'icon_emoji' => $mattermost['bot_icon_emoji'], + 'text' => mb_convert_encoding($messageContent, 'UTF-8', 'auto'), + ); + + $data_string = json_encode($data); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $webhook_url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Type: application/json' + ), + ); + + $result = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($result === false) { + echo "Error sending Mattermost notifications: " . curl_error($ch) . "
"; + } else { + $resultData = json_decode($result, true); + if (isset($resultData['code']) && $resultData['code'] == 200) { + echo "Mattermost Notifications sent successfully
"; + } else { + $errorMsg = isset($resultData['msg']) ? $resultData['msg'] : 'Unknown error'; + echo "Mattermost API error: " . $errorMsg . "
"; + } + } + curl_close($ch); } - curl_close($ch); } } @@ -702,119 +718,129 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) { // Ntfy notifications if enabled if ($ntfyNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); + $ssrf = is_url_safe_for_ssrf($ntfy['host'], $db); + if (!$ssrf) { + echo "SSRF attempt detected for Ntfy host URL. Notifications not sent.
"; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); - if ($user['name']) { - $message = $user['name'] . ", the following subscriptions are up for renewal:\n"; - } else { - $message = "The following subscriptions are up for renewal:\n"; - } + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for renewal:\n"; + } else { + $message = "The following subscriptions are up for renewal:\n"; + } - foreach ($perUser as $subscription) { - $dayText = getDaysText($subscription['days']); - $message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; - } + foreach ($perUser as $subscription) { + $dayText = getDaysText($subscription['days']); + $message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n"; + } - $headers = json_decode($ntfy["headers"], true); - $customheaders = []; + $headers = json_decode($ntfy["headers"], true); + $customheaders = []; - if (is_array($headers)) { - $customheaders = array_map(function ($key, $value) { - return "$key: $value"; - }, array_keys($headers), $headers); - } + if (is_array($headers)) { + $customheaders = array_map(function ($key, $value) { + return "$key: $value"; + }, array_keys($headers), $headers); + } - $ch = curl_init(); + $ch = curl_init(); - $ntfyHost = rtrim($ntfy["host"], '/'); - $ntfyTopic = $ntfy['topic']; + $ntfyHost = rtrim($ntfy["host"], '/'); + $ntfyTopic = $ntfy['topic']; - curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $message); - curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - if ($ntfy['ignore_ssl']) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - } + if ($ntfy['ignore_ssl']) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } - $response = curl_exec($ch); - curl_close($ch); + $response = curl_exec($ch); + curl_close($ch); - if ($response === false) { - echo "Error sending notifications: " . curl_error($ch) . "
"; - } else { - echo "Ntfy Notifications sent
"; + if ($response === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Ntfy Notifications sent
"; + } } } } // Webhook notifications if enabled if ($webhookNotificationsEnabled) { - foreach ($notify as $userId => $perUser) { - // Get name of user from household table - $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); - $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); - $result = $stmt->execute(); - $user = $result->fetchArray(SQLITE3_ASSOC); - - if ($user['name']) { - $payer = $user['name']; - } - - foreach ($perUser as $subscription) { - // Ensure the payload is reset for each subscription - $payload = $webhook['payload']; - $payload = str_replace("{{days_until}}", $days, $payload); - $payload = str_replace("{{subscription_name}}", $subscription['name'], $payload); - $payload = str_replace("{{subscription_price}}", $subscription['formatted_price'], $payload); - $payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload); - $payload = str_replace("{{subscription_category}}", $subscription['category'], $payload); - $payload = str_replace("{{subscription_payer}}", $payer, $payload); // Use $payer instead of $subscription['payer'] - $payload = str_replace("{{subscription_date}}", $subscription['date'], $payload); - $payload = str_replace("{{subscription_days_until_payment}}", $subscription['days'], $payload); - $payload = str_replace("{{subscription_url}}", $subscription['url'], $payload); - $payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload); - - // Initialize cURL for each subscription - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $webhook['url']); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - - // Add headers if they exist - if (!empty($webhook['headers'])) { - $customheaders = json_decode($webhook["headers"], true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + $ssrf = is_url_safe_for_ssrf($webhook['url'], $db); + if (!$ssrf) { + echo "SSRF attempt detected for webhook URL. Notifications not sent.
";; + } else { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if ($user['name']) { + $payer = $user['name']; } - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - // Handle SSL settings - if ($webhook['ignore_ssl']) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + + foreach ($perUser as $subscription) { + // Ensure the payload is reset for each subscription + $payload = $webhook['payload']; + $payload = str_replace("{{days_until}}", $days, $payload); + $payload = str_replace("{{subscription_name}}", $subscription['name'], $payload); + $payload = str_replace("{{subscription_price}}", $subscription['formatted_price'], $payload); + $payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload); + $payload = str_replace("{{subscription_category}}", $subscription['category'], $payload); + $payload = str_replace("{{subscription_payer}}", $payer, $payload); // Use $payer instead of $subscription['payer'] + $payload = str_replace("{{subscription_date}}", $subscription['date'], $payload); + $payload = str_replace("{{subscription_days_until_payment}}", $subscription['days'], $payload); + $payload = str_replace("{{subscription_url}}", $subscription['url'], $payload); + $payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload); + + // Initialize cURL for each subscription + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $webhook['url']); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + + // Add headers if they exist + if (!empty($webhook['headers'])) { + $customheaders = json_decode($webhook["headers"], true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + } + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Handle SSL settings + if ($webhook['ignore_ssl']) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } + + // Execute the cURL request + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($response === false || $httpCode >= 400) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Webhook Notification sent for subscription: " . $subscription['name'] . "
"; + } + + usleep(1000000); // 1s delay between requests } - - // Execute the cURL request - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($response === false || $httpCode >= 400) { - echo "Error sending notifications: " . curl_error($ch) . "
"; - } else { - echo "Webhook Notification sent for subscription: " . $subscription['name'] . "
"; - } - - usleep(1000000); // 1s delay between requests } } } diff --git a/endpoints/logos/search.php b/endpoints/logos/search.php index f89aa63..d443343 100644 --- a/endpoints/logos/search.php +++ b/endpoints/logos/search.php @@ -2,83 +2,121 @@ if (isset($_GET['search'])) { $searchTerm = urlencode($_GET['search'] . " logo"); - $url = "https://www.google.com/search?q={$searchTerm}&tbm=isch&tbs=iar:xw,ift:png"; - $backupUrl = "https://search.brave.com/search?q={$searchTerm}"; + function applyProxy($ch) { + $proxy = getenv('https_proxy') + ?: getenv('HTTPS_PROXY') + ?: getenv('http_proxy') + ?: getenv('HTTP_PROXY') + ?: null; - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - - // Convert all environment variable keys to lowercase - $envVars = array_change_key_case($_SERVER, CASE_LOWER); - - // Check for http_proxy or https_proxy environment variables - $httpProxy = isset($envVars['http_proxy']) ? $envVars['http_proxy'] : null; - $httpsProxy = isset($envVars['https_proxy']) ? $envVars['https_proxy'] : null; - - if (!empty($httpProxy)) { - curl_setopt($ch, CURLOPT_PROXY, $httpProxy); - } elseif (!empty($httpsProxy)) { - curl_setopt($ch, CURLOPT_PROXY, $httpsProxy); + if ($proxy) { + curl_setopt($ch, CURLOPT_PROXY, $proxy); + } } - $response = curl_exec($ch); - if ($response === false) { - // If cURL fails to access google images, use brave image search as a backup + function curlGet($url, $headers = []) { + $allowedHosts = ['duckduckgo.com', 'search.brave.com']; + $host = parse_url($url, PHP_URL_HOST); + if (!in_array($host, $allowedHosts)) return null; + $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $backupUrl); + curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - $envVars = array_change_key_case($_SERVER, CASE_LOWER); - $httpProxy = isset($envVars['http_proxy']) ? $envVars['http_proxy'] : null; - $httpsProxy = isset($envVars['https_proxy']) ? $envVars['https_proxy'] : null; - if (!empty($httpProxy)) { - curl_setopt($ch, CURLOPT_PROXY, $httpProxy); - } elseif (!empty($httpsProxy)) { - curl_setopt($ch, CURLOPT_PROXY, $httpsProxy); - } - $response = curl_exec($ch); - if ($response === false) { - echo json_encode(['error' => 'Failed to fetch data from Google.']); - } else { - $imageUrls = extractImageUrlsFromPage($response); - header('Content-Type: application/json'); - echo json_encode(['imageUrls' => $imageUrls]); - } - } else { - // Parse the HTML response to extract image URLs - $imageUrls = extractImageUrlsFromPage($response); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + + // Explicitly disable proxy by default, then re-apply only from env (not $_SERVER) + curl_setopt($ch, CURLOPT_PROXY, ''); + curl_setopt($ch, CURLOPT_NOPROXY, '*'); - // Pass the image URLs to the client - header('Content-Type: application/json'); - echo json_encode(['imageUrls' => $imageUrls]); + if (!empty($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + applyProxy($ch); + $response = curl_exec($ch); + curl_close($ch); + return $response ?: null; } - curl_close($ch); -} else { - echo json_encode(['error' => 'Invalid request.']); -} + function getVqdToken($query) { + $html = curlGet("https://duckduckgo.com/?q={$query}&ia=images"); + if ($html && preg_match('/vqd="?([\d-]+)"?/', $html, $matches)) { + return $matches[1]; + } + return null; + } -function extractImageUrlsFromPage($html) -{ - $imageUrls = []; + function fetchDDGImages($query, $vqd) { + $params = http_build_query([ + 'l' => 'us-en', + 'o' => 'json', + 'q' => urldecode($query), + 'vqd' => $vqd, + 'f' => ',,transparent,Wide,', + 'p' => '1', + ]); + + $response = curlGet("https://duckduckgo.com/i.js?{$params}", [ + 'Accept: application/json', + 'Referer: https://duckduckgo.com/', + ]); + + if (!$response) return null; + + $data = json_decode($response, true); + if (!isset($data['results']) || empty($data['results'])) return null; + + return array_column($data['results'], 'image'); + } + + function fetchBraveImages($query) { + $url = "https://search.brave.com/images?q={$query}"; + $html = curlGet($url, [ + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language: en-US,en;q=0.5', + 'Referer: https://search.brave.com/', + ]); + + if (!$html) return null; $doc = new DOMDocument(); @$doc->loadHTML($html); + $imageUrls = []; $imgTags = $doc->getElementsByTagName('img'); foreach ($imgTags as $imgTag) { $src = $imgTag->getAttribute('src'); - if (!strstr($imgTag->getAttribute('class'), "favicon") && !strstr($imgTag->getAttribute('class'), "logo")) { - if (filter_var($src, FILTER_VALIDATE_URL)) { - $imageUrls[] = $src; - } - } + $class = $imgTag->getAttribute('class'); + + if (str_contains($class, 'favicon') || str_contains($class, 'logo')) continue; + if (!filter_var($src, FILTER_VALIDATE_URL)) continue; + if (str_contains($src, 'cdn.search.brave.com')) continue; // filter Brave UI assets + + $imageUrls[] = $src; } - return $imageUrls; + return !empty($imageUrls) ? $imageUrls : null; } -?> \ No newline at end of file + // --- Main flow --- + + // Try DuckDuckGo first + $vqd = getVqdToken($searchTerm); + $imageUrls = $vqd ? fetchDDGImages($searchTerm, $vqd) : null; + + // Fall back to Brave if DDG failed at any step + if (!$imageUrls) { + $imageUrls = fetchBraveImages($searchTerm); + } + + header('Content-Type: application/json'); + + if ($imageUrls) { + echo json_encode(['imageUrls' => $imageUrls]); + } else { + echo json_encode(['error' => 'Failed to fetch images from both DuckDuckGo and Brave.']); + } + +} else { + echo json_encode(['error' => 'Invalid request.']); +} +?> diff --git a/endpoints/notifications/savediscordnotifications.php b/endpoints/notifications/savediscordnotifications.php index 1a3a2e1..b1a0a45 100644 --- a/endpoints/notifications/savediscordnotifications.php +++ b/endpoints/notifications/savediscordnotifications.php @@ -2,6 +2,7 @@ require_once '../../includes/connect_endpoint.php'; require_once '../../includes/validate_endpoint.php'; +require_once '../../includes/ssrf_helper.php'; $postData = file_get_contents("php://input"); $data = json_decode($postData, true); @@ -20,6 +21,8 @@ if ( $bot_username = $data["bot_username"]; $bot_avatar_url = $data["bot_avatar"]; + validate_webhook_url_for_ssrf($webhook_url, $db, $i18n); + $query = "SELECT COUNT(*) FROM discord_notifications WHERE user_id = :userId"; $stmt = $db->prepare($query); $stmt->bindParam(":userId", $userId, SQLITE3_INTEGER); diff --git a/endpoints/notifications/savegotifynotifications.php b/endpoints/notifications/savegotifynotifications.php index bd452cf..5ce40af 100644 --- a/endpoints/notifications/savegotifynotifications.php +++ b/endpoints/notifications/savegotifynotifications.php @@ -1,6 +1,7 @@ prepare($query); $stmt->bindParam(":userId", $userId, SQLITE3_INTEGER); diff --git a/endpoints/notifications/savemattermostnotifications.php b/endpoints/notifications/savemattermostnotifications.php index 6faa2f4..b2c97b2 100755 --- a/endpoints/notifications/savemattermostnotifications.php +++ b/endpoints/notifications/savemattermostnotifications.php @@ -1,6 +1,7 @@ false, + "message" => translate("error", $i18n) + ])); + } + + validate_webhook_url_for_ssrf($webhook_url, $db, $i18n); + $query = "SELECT COUNT(*) FROM mattermost_notifications WHERE user_id = :userId"; $stmt = $db->prepare($query); $stmt->bindParam(":userId", $userId, SQLITE3_INTEGER); diff --git a/endpoints/notifications/saventfynotifications.php b/endpoints/notifications/saventfynotifications.php index 401efa0..391e1cc 100644 --- a/endpoints/notifications/saventfynotifications.php +++ b/endpoints/notifications/saventfynotifications.php @@ -2,6 +2,7 @@ require_once '../../includes/connect_endpoint.php'; require_once '../../includes/validate_endpoint.php'; +require_once '../../includes/ssrf_helper.php'; $postData = file_get_contents("php://input"); $data = json_decode($postData, true); @@ -36,6 +37,8 @@ if ( ])); } + validate_webhook_url_for_ssrf($url, $db, $i18n); + $query = "SELECT COUNT(*) FROM ntfy_notifications WHERE user_id = :userId"; $stmt = $db->prepare($query); $stmt->bindParam(":userId", $userId, SQLITE3_INTEGER); diff --git a/endpoints/notifications/savewebhooknotifications.php b/endpoints/notifications/savewebhooknotifications.php index cdd5766..0903b92 100644 --- a/endpoints/notifications/savewebhooknotifications.php +++ b/endpoints/notifications/savewebhooknotifications.php @@ -1,6 +1,7 @@ prepare($query); $stmt->bindParam(":userId", $userId, SQLITE3_INTEGER); diff --git a/endpoints/payments/rename.php b/endpoints/payments/rename.php index fcd8c9a..3f3ada5 100644 --- a/endpoints/payments/rename.php +++ b/endpoints/payments/rename.php @@ -3,6 +3,15 @@ require_once '../../includes/connect_endpoint.php'; require_once '../../includes/validate_endpoint.php'; +function validate($value) +{ + $value = trim($value); + $value = stripslashes($value); + $value = htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + return $value; +} + + if (!isset($_POST['paymentId']) || !isset($_POST['name']) || $_POST['paymentId'] === '' || $_POST['name'] === '') { die(json_encode([ "success" => false, @@ -11,7 +20,14 @@ if (!isset($_POST['paymentId']) || !isset($_POST['name']) || $_POST['paymentId'] } $paymentId = $_POST['paymentId']; -$name = $_POST['name']; +$name = validate($_POST['name']); +if (strlen($name) > 255) { + die(json_encode([ + "success" => false, + "message" => translate('fields_missing', $i18n) + ])); +} + $sql = "UPDATE payment_methods SET name = :name WHERE id = :paymentId and user_id = :userId"; $stmt = $db->prepare($sql); @@ -20,16 +36,11 @@ $stmt->bindParam(':paymentId', $paymentId, SQLITE3_INTEGER); $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER); $result = $stmt->execute(); -if ($result) { - echo json_encode([ - "success" => true, - "message" => translate('payment_renamed', $i18n) - ]); +if ($result && $db->changes() > 0) { + echo json_encode(["success" => true, "message" => translate('payment_renamed', $i18n)]); } else { - echo json_encode([ - "success" => false, - "message" => translate('payment_not_renamed', $i18n) - ]); + echo json_encode(["success" => false, "message" => translate('payment_not_renamed', $i18n)]); } + ?> \ No newline at end of file diff --git a/endpoints/payments/search.php b/endpoints/payments/search.php index b88a08b..ff68846 100644 --- a/endpoints/payments/search.php +++ b/endpoints/payments/search.php @@ -1,84 +1,130 @@ 'Failed to fetch data from Google.']); - } else { - $imageUrls = extractImageUrlsFromPage($response); - header('Content-Type: application/json'); - echo json_encode(['imageUrls' => $imageUrls]); - } - } else { - // Parse the HTML response to extract image URLs - $imageUrls = extractImageUrlsFromPage($response); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'); - // Pass the image URLs to the client - header('Content-Type: application/json'); - echo json_encode(['imageUrls' => $imageUrls]); + if (!empty($headers)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + applyProxy($ch); + $response = curl_exec($ch); + curl_close($ch); + + return $response ?: null; + } + + $searchTermRaw = $_GET['search'] . " logo"; + $searchTerm = urlencode($searchTermRaw); + + function getVqdToken($query) { + $html = curlGet("https://duckduckgo.com/?q={$query}&ia=images"); + if ($html && preg_match('/vqd="?([\d-]+)"?/', $html, $matches)) { + return $matches[1]; + } + return null; + } + + function fetchDDGImages($query, $vqd) { + $params = http_build_query([ + 'l' => 'us-en', + 'o' => 'json', + 'q' => urldecode($query), + 'vqd' => $vqd, + 'f' => ',,,,', // size,color,type,layout,license → all unset + 'p' => '1', // safesearch on + ]); + + $response = curlGet("https://duckduckgo.com/i.js?{$params}", [ + 'Accept: application/json', + 'Referer: https://duckduckgo.com/', + ]); + + if (!$response) return null; + + $data = json_decode($response, true); + if (!isset($data['results']) || empty($data['results'])) return null; + + return array_column($data['results'], 'image'); + } + + function fetchBraveImages($query) { + $url = "https://search.brave.com/images?q={$query}"; + $html = curlGet($url, [ + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language: en-US,en;q=0.5', + 'Referer: https://search.brave.com/', + ]); + + if (!$html) return null; + + $doc = new DOMDocument(); + @$doc->loadHTML($html); + + $blockedDomains = ['cdn.search.brave.com', 'search.brave.com/static']; + + $imageUrls = []; + $imgTags = $doc->getElementsByTagName('img'); + + foreach ($imgTags as $imgTag) { + $src = $imgTag->getAttribute('src'); + $class = $imgTag->getAttribute('class'); + + if (str_contains($class, 'favicon') || str_contains($class, 'logo')) continue; + if (!filter_var($src, FILTER_VALIDATE_URL)) continue; + + foreach ($blockedDomains as $blocked) { + if (str_contains($src, $blocked)) { + continue 2; // skip to next + } + } + + $imageUrls[] = $src; + } + + return !empty($imageUrls) ? $imageUrls : null; + } + + // Main flow: DDG first, Brave fallback + $vqd = getVqdToken($searchTerm); + $imageUrls = $vqd ? fetchDDGImages($searchTerm, $vqd) : null; + + if (!$imageUrls) { + $imageUrls = fetchBraveImages($searchTerm); + } + + header('Content-Type: application/json'); + + if ($imageUrls) { + echo json_encode(['imageUrls' => $imageUrls]); + } else { + echo json_encode(['error' => 'Failed to fetch images from DuckDuckGo and Brave.']); } - curl_close($ch); } else { echo json_encode(['error' => 'Invalid request.']); } - -function extractImageUrlsFromPage($html) -{ - $imageUrls = []; - - $doc = new DOMDocument(); - @$doc->loadHTML($html); - - $imgTags = $doc->getElementsByTagName('img'); - foreach ($imgTags as $imgTag) { - $src = $imgTag->getAttribute('src'); - if (!strstr($imgTag->getAttribute('class'), "favicon") && !strstr($imgTag->getAttribute('class'), "logo")) { - if (filter_var($src, FILTER_VALIDATE_URL)) { - $imageUrls[] = $src; - } - } - } - - return $imageUrls; -} - -?> \ No newline at end of file diff --git a/includes/i18n/languages.php b/includes/i18n/languages.php index 92b3645..a4a51ed 100644 --- a/includes/i18n/languages.php +++ b/includes/i18n/languages.php @@ -19,6 +19,7 @@ $languages = [ "pl" => ["name" => "Polski", "dir" => "ltr"], "pt" => ["name" => "Português", "dir" => "ltr"], "pt_br" => ["name" => "Português Brasileiro", "dir" => "ltr"], + "ro" => ["name" => "Română", "dir" => "ltr"], "ru" => ["name" => "Русский", "dir" => "ltr"], "sl" => ["name" => "Slovenščina", "dir" => "ltr"], "sr_lat" => ["name" => "Srpski", "dir" => "ltr"], @@ -28,8 +29,6 @@ $languages = [ "vi" => ["name" => "Tiếng Việt", "dir" => "ltr"], "zh_cn" => ["name" => "简体中文", "dir" => "ltr"], "zh_tw" => ["name" => "繁體中文", "dir" => "ltr"], - "ro" => ["name" => "Română", "dir" => "ltr"], - ] ?> diff --git a/includes/oidc/oidc_login.php b/includes/oidc/oidc_login.php index 56c5c35..09ecd61 100644 --- a/includes/oidc/oidc_login.php +++ b/includes/oidc/oidc_login.php @@ -29,7 +29,8 @@ $_SESSION['token'] = $token; $cookieValue = $username . "|" . $token . "|" . $main_currency; setcookie('wallos_login', $cookieValue, [ 'expires' => $cookieExpire, - 'samesite' => 'Strict' + 'samesite' => 'Strict', + 'httponly' => true, ]); // Set language cookie diff --git a/includes/ssrf_helper.php b/includes/ssrf_helper.php index 5d66520..e608e9e 100644 --- a/includes/ssrf_helper.php +++ b/includes/ssrf_helper.php @@ -1,5 +1,17 @@ = ip2long('100.64.0.0') + && $long <= ip2long('100.127.255.255'); +} + /** * Validates a webhook URL against SSRF attacks and checks the admin allowlist. * If validation fails, it kills the script and outputs a JSON error response. @@ -35,7 +47,7 @@ function validate_webhook_url_for_ssrf($url, $db, $i18n) { $ipWithPort = $port ? $ip . ':' . $port : $ip; // Check if it's a private IP - $is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false; + $is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false || is_cgnat_ip($ip); if ($is_private) { $stmt = $db->prepare("SELECT local_webhook_notifications_allowlist FROM admin LIMIT 1"); @@ -65,4 +77,61 @@ function validate_webhook_url_for_ssrf($url, $db, $i18n) { 'ip' => $ip, 'port' => $targetPort ]; -} \ No newline at end of file +} + +/** + * Non-fatal variant for use in cron jobs (sendnotifications.php). + * Returns the same ['host', 'ip', 'port'] array on success, or false on failure. + * Never calls die() — caller should use continue/skip on false. + * Respects the admin allowlist for private IPs, just like the main function. + * + * @param string $url The destination URL to check + * @param SQLite3 $db The database connection + * @return array|false + */ +function is_url_safe_for_ssrf($url, $db) { + $parsedUrl = parse_url($url); + if (!$parsedUrl || !isset($parsedUrl['host'])) return false; + + $scheme = strtolower($parsedUrl['scheme'] ?? ''); + if (!in_array($scheme, ['http', 'https'])) return false; + + $urlHost = $parsedUrl['host']; + $port = $parsedUrl['port'] ?? ''; + $ip = gethostbyname($urlHost); + + // DNS failure + if ($ip === $urlHost && filter_var($urlHost, FILTER_VALIDATE_IP) === false) return false; + + $hostWithPort = $port ? $urlHost . ':' . $port : $urlHost; + $ipWithPort = $port ? $ip . ':' . $port : $ip; + + $is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false + || is_cgnat_ip($ip); + + if ($is_private) { + $stmt = $db->prepare("SELECT local_webhook_notifications_allowlist FROM admin LIMIT 1"); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + $allowlist_str = $row ? $row['local_webhook_notifications_allowlist'] : ''; + $allowlist = array_filter(array_map('trim', explode(',', $allowlist_str))); + + if ( + !in_array($urlHost, $allowlist) && + !in_array($ip, $allowlist) && + !in_array($hostWithPort, $allowlist) && + !in_array($ipWithPort, $allowlist) + ) { + return false; // private and not in allowlist — skip silently + } + } + + $targetPort = $port ?: ($scheme === 'https' ? 443 : 80); + + return [ + 'host' => $urlHost, + 'ip' => $ip, + 'port' => $targetPort + ]; +} diff --git a/includes/version.php b/includes/version.php index 8ec1516..8bfe422 100644 --- a/includes/version.php +++ b/includes/version.php @@ -1,3 +1,3 @@ \ No newline at end of file diff --git a/login.php b/login.php index 58d0449..8a577b3 100644 --- a/login.php +++ b/login.php @@ -72,13 +72,14 @@ if ($adminRow['login_disabled'] == 1) { $settings = $result->fetchArray(SQLITE3_ASSOC); setcookie('colorTheme', $settings['color_theme'], [ 'expires' => $cookieExpire, - 'samesite' => 'Strict' + 'samesite' => 'Strict', ]); $cookieValue = $username . "|" . "abc123ABC" . "|" . $main_currency; setcookie('wallos_login', $cookieValue, [ 'expires' => $cookieExpire, - 'samesite' => 'Strict' + 'samesite' => 'Strict', + 'httponly' => true, ]); $db->close(); @@ -198,7 +199,8 @@ if (isset($_POST['username']) && isset($_POST['password'])) { $cookieValue = $username . "|" . $token . "|" . $main_currency; setcookie('wallos_login', $cookieValue, [ 'expires' => $cookieExpire, - 'samesite' => 'Strict' + 'samesite' => 'Strict', + 'httponly' => true, ]); } diff --git a/settings.php b/settings.php index 9410de7..b28054c 100644 --- a/settings.php +++ b/settings.php @@ -1097,14 +1097,13 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol']; class="" placeholder="" value="" /> - - + diff --git a/styles/styles.css b/styles/styles.css index 1df103e..b695d3e 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -28,6 +28,7 @@ textarea { font-weight: 400; } +button.hidden, input.hidden { display: none; } @@ -281,6 +282,11 @@ main>.contain { font-size: 12px; } +.button.icon-button, +.button-secondary.icon-button { + padding: 15px; +} + button:hover svg .main-color { fill: var(--hover-color); }