[TikiWiki-commits] [Git][tikiwiki/tiki][30.x] [BP][NEW] Introduce a centralized SSRF protection library in Tiki

"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a10aa2d6f427_38192724590ec@gitlab-sidekiq-low-urgency-cpu-bound-v2-5755d7f9f9-g9bsh.mail>

Elifeleti Mukisa Dan pushed to branch 30.x at Tiki Wiki CMS Groupware / Tiki


Commits:
2c8a9f7e by Elifeleti Mukisa Dan at 2026-05-22T19:02:57+00:00
[BP][NEW] Introduce a centralized SSRF protection library in Tiki
---
* [NEW] Introduce a centralized SSRF protection library in Tiki
---
* [NEW] Introduce a centralized SSRF protection library in Tiki


(cherry picked from commit bdf50591016dcda26c10ad9ccce83bf22a38f2eb)

322f139b [FIX] Implement a URL blocklist that rejects private IP range
6f5fd32b [NEW] Add centralized SSRF protection for server-side URL fetches
755d5f1c [FIX] Block SSRF attacks in directory URL validation

Co-authored-by: Elifeleti Mukisa Dan <[email protected]>

See merge request tikiwiki/tiki!10291


(cherry picked from commit e06674bc84d2a10faa1a65b5c638a89f55e5ed88)

d9b75deb [NEW] Introduce a centralized SSRF protection library in Tiki

Co-authored-by: Elifeleti Mukisa Dan <[email protected]>

See merge request tikiwiki/tiki!10310

- - - - -


13 changed files:

- lib/Importer/WikiMediawiki.php
- lib/OpenIdConnect/OpenIdConnectLib.php
- lib/avatarlib.php
- + lib/core/Tiki/Security/SsrfLib.php
- + lib/prefs/ssrf.php
- lib/videogals/peertubelib.php
- lib/wiki-plugins/wikiplugin_fancylink.php
- lib/wiki-plugins/wikiplugin_includeurl.php
- lib/wiki-plugins/wikiplugin_oembed.php
- lib/wiki-plugins/wikiplugin_sheet.php
- templates/admin/include_security.tpl
- tiki-directory_add_site.php
- tiki-directory_add_tiki_site.php


Changes:

=====================================
lib/Importer/WikiMediawiki.php
=====================================
@@ -344,6 +344,18 @@ class WikiMediawiki extends Wiki
                     continue;
                 }
 
+                // Prevent SSRF: attachment URLs come from the imported XML dump
+                // which may be untrusted. Block private/reserved IP targets.
+                // Only check URLs with a scheme (http/https); relative file paths
+                // are local references and not a network SSRF vector.
+                if (preg_match('#^https?://#i', $fileUrl)) {
+                    $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+                    if (! $ssrf->isUrlAllowed($fileUrl)) {
+                        $this->saveAndDisplayLog(tr('File %0 not imported: URL targets a private or reserved address.', $fileName) . "\n", true);
+                        continue;
+                    }
+                }
+
                 if (@fopen($fileUrl, 'r')) {
                     $attachmentContent = @file_get_contents($fileUrl);
                     $newFile = fopen($this->attachmentsDestDir . $fileName, 'w');


=====================================
lib/OpenIdConnect/OpenIdConnectLib.php
=====================================
@@ -154,6 +154,12 @@ class OpenIdConnectLib
                 $jwkArr = unserialize($cachedValue);
                 return $jwkArr;
             } else {
+                // Validate JWKS URL to prevent SSRF
+                $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+                if (! $ssrf->isUrlAllowed($this->jwksUrl)) {
+                    throw new \Exception('JWKS URL is not allowed');
+                }
+
                 $jwkArr = file_get_contents($this->jwksUrl);
 
                 if ($jwkArr === false) {


=====================================
lib/avatarlib.php
=====================================
@@ -47,6 +47,12 @@ class AvatarLib extends TikiLib
             $userwatch = $user;
         }
 
+        // Validate URL to prevent SSRF via user-supplied avatar URLs
+        $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+        if (! $ssrf->isUrlAllowed($url)) {
+            throw new \Exception('Avatar URL is not allowed');
+        }
+
         $data = file_get_contents($url);
         list($iwidth, $iheight, $itype, $iattr) = getimagesize($url);
         $itype = image_type_to_mime_type($itype);


=====================================
lib/core/Tiki/Security/SsrfLib.php
=====================================
@@ -0,0 +1,153 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+namespace Tiki\Security;
+
+/**
+ * SSRF protection utilities.
+ *
+ * Validates hosts and URLs before server-side fetches to prevent
+ * Server-Side Request Forgery via private/reserved IP ranges,
+ * DNS rebinding, and redirect chains.
+ *
+ * Supports a configurable whitelist so that specific internal hosts
+ * can be explicitly permitted when needed.
+ */
+class SsrfLib
+{
+    /** @var string[] Hosts that are always allowed regardless of IP checks */
+    private array $whitelistedHosts = [];
+
+    /**
+     * @param string[] $whitelistedHosts Hostnames that bypass the private IP check
+     */
+    public function __construct(array $whitelistedHosts = [])
+    {
+        $this->whitelistedHosts = array_map('strtolower', $whitelistedHosts);
+    }
+
+    /**
+     * Return an instance pre-configured with the Tiki admin whitelist preference.
+     *
+     * The preference `ssrf_whitelisted_hosts` is expected to be a
+     * comma-separated string of hostnames stored in the Tiki preferences
+     * (Admin → Security).  When the preference does not exist the
+     * whitelist is empty.
+     */
+    public static function fromPrefs(): self
+    {
+        global $prefs;
+        $raw = ! empty($prefs['ssrf_whitelisted_hosts']) ? $prefs['ssrf_whitelisted_hosts'] : '';
+        $hosts = array_filter(array_map('trim', explode(',', $raw)));
+        return new self($hosts);
+    }
+
+    /**
+     * Check whether a host is allowed to be fetched by the server.
+     *
+     * Rejects hosts that resolve to private, loopback, link-local or
+     * other reserved IP ranges.  If a hostname resolves to multiple
+     * addresses, **any** private/reserved address causes rejection.
+     *
+     * Whitelisted hosts bypass the check entirely.
+     *
+     * @param string $host Hostname or IP literal
+     * @return bool True if allowed, false if disallowed
+     */
+    public function isHostAllowed(string $host): bool
+    {
+        if ($host === '') {
+            return false;
+        }
+
+        // Whitelist check (case-insensitive)
+        if (in_array(strtolower($host), $this->whitelistedHosts, true)) {
+            return true;
+        }
+
+        // IP literal — validate directly
+        if (filter_var($host, FILTER_VALIDATE_IP)) {
+            return filter_var(
+                $host,
+                FILTER_VALIDATE_IP,
+                FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
+            ) !== false;
+        }
+
+        $ips = $this->resolveHost($host);
+
+        // If DNS returned nothing, allow (let the fetch itself fail)
+        if (empty($ips)) {
+            return true;
+        }
+
+        foreach ($ips as $ip) {
+            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Validate a full URL: scheme must be http(s), and the resolved
+     * host must pass {@see isHostAllowed()}.
+     *
+     * @param string $url
+     * @return bool
+     */
+    public function isUrlAllowed(string $url): bool
+    {
+        $url = trim($url);
+
+        if ($url === '' || ! filter_var($url, FILTER_VALIDATE_URL)) {
+            return false;
+        }
+
+        $parsed = parse_url($url);
+        if (! $parsed || empty($parsed['scheme']) || empty($parsed['host'])) {
+            return false;
+        }
+
+        if (! in_array(strtolower($parsed['scheme']), ['http', 'https'], true)) {
+            return false;
+        }
+
+        return $this->isHostAllowed($parsed['host']);
+    }
+
+    /**
+     * Resolve a hostname to all its A and AAAA records.
+     *
+     * @param string $host
+     * @return string[] IP addresses
+     */
+    private function resolveHost(string $host): array
+    {
+        $ips = [];
+
+        // IPv4 A records
+        $a = @gethostbynamel($host);
+        if (is_array($a)) {
+            $ips = $a;
+        }
+
+        // IPv6 AAAA records
+        if (function_exists('dns_get_record')) {
+            $aaaa = @dns_get_record($host, DNS_AAAA);
+            if (is_array($aaaa)) {
+                foreach ($aaaa as $rec) {
+                    if (! empty($rec['ipv6'])) {
+                        $ips[] = $rec['ipv6'];
+                    }
+                }
+            }
+        }
+
+        return $ips;
+    }
+}


=====================================
lib/prefs/ssrf.php
=====================================
@@ -0,0 +1,20 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+function prefs_ssrf_list()
+{
+    return [
+        'ssrf_whitelisted_hosts' => [
+            'name' => tra('SSRF whitelisted hosts'),
+            'description' => tra('Comma-separated list of hostnames that are allowed to be fetched server-side even if they resolve to private or reserved IP addresses. Leave empty to disallow all private ranges.'),
+            'type' => 'textarea',
+            'size' => 3,
+            'filter' => 'text',
+            'default' => '',
+            'tags' => ['advanced', 'security'],
+        ],
+    ];
+}


=====================================
lib/videogals/peertubelib.php
=====================================
@@ -69,6 +69,14 @@ class PeerTubeLib
 
     private function makeRequest($method, $url, $data = [], $auth = true, $rawBody = false)
     {
+        // Prevent SSRF: validate the request URL before cURL opens the connection.
+        // PeerTube base URL is admin-configured, but CURLOPT_FOLLOWLOCATION could
+        // follow redirects to internal hosts if the remote instance is compromised.
+        $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+        if (! $ssrf->isUrlAllowed($url)) {
+            throw new \Exception('PeerTube request blocked: URL targets a private or reserved address.');
+        }
+
         $ch = curl_init();
         $method = strtoupper($method);
         curl_setopt($ch, CURLOPT_URL, $url);


=====================================
lib/wiki-plugins/wikiplugin_fancylink.php
=====================================
@@ -513,6 +513,11 @@ function extractUrlMetadata($url, $cacheTime = 86400)
         return false;
     }
 
+    // Prevent SSRF by validating the URL and resolved hosts.
+    $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+    if (! $ssrf->isUrlAllowed($url)) {
+        return false;
+    }
     // Check cache first
     $cacheLib = TikiLib::lib('cache');
     // Use the full URL in the cache key to avoid collisions between different domains
@@ -722,6 +727,8 @@ function resolveUrl($url, $baseUrl)
     return $scheme . $host . $port . $path . $url;
 }
 
+
+
 /**
  * Truncate text to a specified length
  *


=====================================
lib/wiki-plugins/wikiplugin_includeurl.php
=====================================
@@ -36,6 +36,14 @@ function wikiplugin_includeurl($data, $params)
         return tr('Missing parameter url for plugin %0', 'includeurl') . '<br>';
     } else {
         $url = $params['url'];
+
+        // Prevent SSRF: the response body is rendered into the wiki page,
+        // so block requests to private/reserved IPs and non-http(s) schemes.
+        $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+        if (! $ssrf->isUrlAllowed($url)) {
+            return tra('URL is not allowed (targets a private or reserved address).');
+        }
+
         $html = file_get_contents($url);
 
         // Only include the body part of the html file


=====================================
lib/wiki-plugins/wikiplugin_oembed.php
=====================================
@@ -128,6 +128,14 @@ function getOEmbedData($url)
     if (! filter_var($url, FILTER_VALIDATE_URL)) {
         throw new Exception(tr('The provided URL is not a valid URL.'));
     }
+
+    // Prevent SSRF: the user-supplied URL determines which host the server contacts
+    // for oEmbed discovery. Block private/reserved IPs and non-http(s) schemes.
+    $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+    if (! $ssrf->isUrlAllowed($url)) {
+        throw new Exception(tra('URL is not allowed (targets a private or reserved address).'));
+    }
+
     $parsedUrl = parse_url($url);
     $protocol = $parsedUrl['scheme'];
     $domain = $parsedUrl['host'];


=====================================
lib/wiki-plugins/wikiplugin_sheet.php
=====================================
@@ -241,6 +241,11 @@ EOF;
             $ret = $grid->getTableHtml(true, null, false);
         } else {
             $sheet->parseValues = true;
+            // Validate URL to prevent SSRF
+            $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+            if (! $ssrf->isUrlAllowed($url)) {
+                return tra('Sheet URL is not allowed');
+            }
             $ret = file_get_contents($url);
         }
     } else {


=====================================
templates/admin/include_security.tpl
=====================================
@@ -82,6 +82,10 @@
                 {preference name=http_use_curl}
                 {preference name=feature_debug_console}
             </fieldset>
+            <fieldset>
+                <legend class="h3">{tr}SSRF Protection{/tr}</legend>
+                {preference name=ssrf_whitelisted_hosts}
+            </fieldset>
             <fieldset>
                 <legend class="h3">{tr}Trackers Security{/tr}</legend>
                 {preference name=tracker_adminonlyviewedititem_by_default}


=====================================
tiki-directory_add_site.php
=====================================
@@ -103,9 +103,16 @@ if (isset($_REQUEST["save"])) {
             $msg .= tra("URL already added to the directory. Duplicate site? ");
         }
         if ($prefs['directory_validate_urls'] == 'y') {
-            @$fsh = fopen($_REQUEST['url'], 'r');
-            if (! $fsh) {
-                $msg .= tra("URL cannot be accessed wrong URL or site is offline and cannot be added to the directory. ");
+            // Prevent SSRF: validate the submitted URL before the server opens a
+            // connection to check if the site is reachable.
+            $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+            if (! $ssrf->isUrlAllowed($_REQUEST['url'])) {
+                $msg .= tra("URL is not allowed (targets a private or reserved address). ");
+            } else {
+                @$fsh = fopen($_REQUEST['url'], 'r');
+                if (! $fsh) {
+                    $msg .= tra("URL cannot be accessed wrong URL or site is offline and cannot be added to the directory. ");
+                }
             }
         }
     }


=====================================
tiki-directory_add_tiki_site.php
=====================================
@@ -52,6 +52,12 @@ if ($dirlib->dir_url_exists($_REQUEST['url'])) {
     Feedback::errorAndDie(tra("URL already added to the directory. Duplicate site?"), \Laminas\Http\Response::STATUS_CODE_409);
 }
 if ($prefs['directory_validate_urls'] == 'y') {
+    // Prevent SSRF: validate the submitted URL before the server opens a
+    // connection to check if the site is reachable.
+    $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+    if (! $ssrf->isUrlAllowed($_REQUEST['url'])) {
+        Feedback::errorAndDie(tra("URL is not allowed (targets a private or reserved address)"), \Laminas\Http\Response::STATUS_CODE_400);
+    }
     @$fsh = fopen($_REQUEST['url'], 'r');
     if (! $fsh) {
         Feedback::errorAndDie(tra("URL cannot be accessed: wrong URL or site is offline and cannot be added to the directory"), \Laminas\Http\Response::STATUS_CODE_400);



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/2c8a9f7ec958884793b78d80e3474ad59f84871e

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/2c8a9f7ec958884793b78d80e3474ad59f84871e
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.