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="= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'hidden' : '' ?>"
placeholder="= translate('api_key', $i18n) ?>"
value="= isset($aiSettings['api_key']) ? htmlspecialchars($aiSettings['api_key']) : '' ?>" />
-
-
+
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);
}