public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/dokuwiki] f43: Backport some more security patches
@ 2026-07-22 20:17 Artur Frenszek-Iwicki
  0 siblings, 0 replies; only message in thread
From: Artur Frenszek-Iwicki @ 2026-07-22 20:17 UTC (permalink / raw)
  To: git-commits

A new commit has been pushed.

Repo   : rpms/dokuwiki
Branch : f43
Commit : 8088820e0a7b717ddd351ac32e5632163e675b22
Author : Artur Frenszek-Iwicki <fedora@svgames.pl>
Date   : 2026-07-22T22:15:21+02:00
Stats  : +727/-1 in 3 file(s)
URL    : https://src.fedoraproject.org/rpms/dokuwiki/c/8088820e0a7b717ddd351ac32e5632163e675b22?branch=f43

Log:
Backport some more security patches

---
diff --git a/4703.patch b/4703.patch
new file mode 100644
index 0000000..2b3c80b
--- /dev/null
+++ b/4703.patch
@@ -0,0 +1,672 @@
+From bf2ca3bc0fd40d67eaaec09dfe5a463890ac3d52 Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 11:22:18 +0200
+Subject: [PATCH 1/8] security(authplain): escape username in modifyUser regex
+
+modifyUser() built its PCRE pattern from the raw username, so a username
+containing a regex metacharacter matched other accounts when the user
+updated their own profile. A dot survives cleanUser(), so a username like
+a.b removed every line matching /^a.b:/ (e.g. a1b, or admin for ad.in),
+destroying other accounts and enabling administrator lockout.
+
+Escape the username with preg_quote, matching deleteUsers().
+
+Severity: medium - on the default backend, a user with a regex-metacharacter
+username who edits their own profile can corrupt or delete other accounts and
+lock out the administrator
+
+fixes #4694
+---
+ lib/plugins/authplain/_test/escaping.test.php | 52 +++++++++++++++++++
+ lib/plugins/authplain/auth.php                |  9 +++-
+ 2 files changed, 60 insertions(+), 1 deletion(-)
+
+diff --git a/lib/plugins/authplain/_test/escaping.test.php b/lib/plugins/authplain/_test/escaping.test.php
+index 111a241767..9668885b65 100644
+--- a/lib/plugins/authplain/_test/escaping.test.php
++++ b/lib/plugins/authplain/_test/escaping.test.php
+@@ -88,6 +88,58 @@ public function testModifyUser() {
+         $this->assertTrue($this->auth->checkPass("testuser", $user['pass']));
+     }
+ 
++    /**
++     * @see testModifyUserRegexMetacharacter
++     */
++    public function provideRegexMetacharacterUsers()
++    {
++        // Only the dot actually survives cleanUser() and is thus reachable as
++        // a stored username through the normal user flow. The other
++        // metacharacters are normally stripped during cleaning and cannot
++        // occur that way, but are exercised here directly to ensure
++        // modifyUser() escapes the username robustly on its own.
++        // [attacker username, sibling that an unescaped /^<attacker>:/ matches]
++        return [
++            ['a.b', 'a1b'],   // . matches any single character (reachable via cleanUser)
++            ['xy+', 'xyy'],   // + matches the repeated preceding character
++            ['ab?', 'a'],     // ? makes the preceding character optional
++            ['c[d', 'c[d'],   // [ would make the unescaped pattern an invalid regex
++        ];
++    }
++
++    /**
++     * Modifying a user whose name contains a regex metacharacter must only
++     * touch that user's own line and never match or destroy other accounts.
++     *
++     * @param string $attacker username containing a regex metacharacter
++     * @param string $sibling  account an unescaped pattern would also match
++     * @dataProvider provideRegexMetacharacterUsers
++     */
++    public function testModifyUserRegexMetacharacter($attacker, $sibling)
++    {
++        $this->auth->createUser($attacker, "password", "Attacker", "a@example.com");
++        if ($sibling !== $attacker) {
++            $this->auth->createUser($sibling, "password", "Sibling", "s@example.com");
++        }
++        $this->reloadUsers();
++
++        // the attacker updates only their own profile
++        $user = $this->auth->getUserData($attacker);
++        $user['name'] = "Renamed";
++        $this->assertTrue($this->auth->modifyUser($attacker, $user));
++        $this->reloadUsers();
++
++        // the attacker's own change took effect
++        $this->assertEquals("Renamed", $this->auth->getUserData($attacker)['name']);
++
++        // the sibling account must survive untouched
++        $saved = $this->auth->getUserData($sibling);
++        $this->assertNotFalse($saved, "sibling account '$sibling' was destroyed");
++        if ($sibling !== $attacker) {
++            $this->assertEquals("Sibling", $saved['name']);
++        }
++    }
++
+     // really only required for developers to ensure this plugin will
+     // work with systems running on PCRE 6.6 and lower.
+     public function testLineSplit(){
+diff --git a/lib/plugins/authplain/auth.php b/lib/plugins/authplain/auth.php
+index 0b7ee154ed..ae75013931 100644
+--- a/lib/plugins/authplain/auth.php
++++ b/lib/plugins/authplain/auth.php
+@@ -219,7 +219,14 @@ public function modifyUser($user, $changes)
+             $userinfo['grps']
+         );
+ 
+-        if (!io_replaceInFile($config_cascade['plainauth.users']['default'], '/^' . $user . ':/', $userline, true)) {
++        if (
++            !io_replaceInFile(
++                $config_cascade['plainauth.users']['default'],
++                '/^' . preg_quote($user, '/') . ':/',
++                $userline,
++                true
++            )
++        ) {
+             msg('There was an error modifying your user data. You may need to register again.', -1);
+             // FIXME, io functions should be fail-safe so existing data isn't lost
+             $ACT = 'register';
+
+From 09919d815c3089f72962f2fd9d62268e92cbbe4e Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 11:31:58 +0200
+Subject: [PATCH 2/8] security(revert): escape changelog user and ip to prevent
+ stored XSS
+
+The revert admin plugin is manager-accessible and printed the changelog
+username unescaped. Auth backends that inherit the base
+AuthPlugin::cleanUser() no-op (authpdo, authldap, third-party) allow
+HTML-bearing usernames, letting a regular editor plant a payload that
+runs in a manager's or admin's browser when they open the revert tool.
+
+Also escape the IP in the shared RevisionInfo renderer as defense in
+depth against migrated, remote-API or third-party changelog entries.
+
+Severity: medium - a regular editor can store an HTML username that runs in a
+manager's or admin's session when they open the revert tool, but only on auth
+backends that permit HTML in usernames, not the default authplain.
+
+fixes #4695
+---
+ inc/ChangeLog/RevisionInfo.php | 4 ++--
+ lib/plugins/revert/admin.php   | 2 +-
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/inc/ChangeLog/RevisionInfo.php b/inc/ChangeLog/RevisionInfo.php
+index 5881b02969..20b94fcce5 100644
+--- a/inc/ChangeLog/RevisionInfo.php
++++ b/inc/ChangeLog/RevisionInfo.php
+@@ -157,10 +157,10 @@ public function showEditor()
+         if ($this->val('user')) {
+             $html = '<bdi>' . editorinfo($this->val('user')) . '</bdi>';
+             if (auth_ismanager()) {
+-                $html .= ' <bdo dir="ltr">(' . $this->val('ip') . ')</bdo>';
++                $html .= ' <bdo dir="ltr">(' . hsc($this->val('ip')) . ')</bdo>';
+             }
+         } else {
+-            $html = '<bdo dir="ltr">' . $this->val('ip') . '</bdo>';
++            $html = '<bdo dir="ltr">' . hsc($this->val('ip')) . '</bdo>';
+         }
+         return '<span class="user">' . $html . '</span>';
+     }
+diff --git a/lib/plugins/revert/admin.php b/lib/plugins/revert/admin.php
+index 31d9c873f5..d9ba231279 100644
+--- a/lib/plugins/revert/admin.php
++++ b/lib/plugins/revert/admin.php
+@@ -185,7 +185,7 @@ protected function listEdits($filter)
+             echo ' – ' . htmlspecialchars($recent['sum']);
+ 
+             echo ' <span class="user">';
+-                echo $recent['user'] . ' ' . $recent['ip'];
++                echo hsc($recent['user']) . ' ' . hsc($recent['ip']);
+             echo '</span>';
+ 
+             echo '</div>';
+
+From 27916a76ae3a030b3186de6d464a7afa660c8df4 Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 17:29:54 +0200
+Subject: [PATCH 3/8] security(authad): escape username in LDAP filter to
+ prevent injection
+
+adLDAPUsers::info() concatenated the supplied username into an LDAP
+search filter without escaping. The username is reachable unauthenticated
+through the password-reset form, giving a blind LDAP filter injection
+oracle against the directory: a login of *)(objectclass=* alters the
+filter structure instead of being matched literally.
+
+Escape the username with the library's own ldapSlashes() before building
+the filter. As groups() and passwordExpiry() delegate to info(), this
+covers all reachable authad query paths.
+
+Severity: medium - unauthenticated via the password-reset form on Active
+Directory deployments, but a blind, read-only oracle: it allows user
+enumeration and slow bit-by-bit inference of directory attributes, with no
+authentication bypass, privilege escalation, or write access.
+
+fixes #4697
+---
+ lib/plugins/authad/adLDAP/classes/adLDAPUsers.php | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php b/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php
+index d750f10882..f832aa8ae8 100644
+--- a/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php
++++ b/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php
+@@ -225,10 +225,10 @@ public function info($username, $fields = NULL, $isGUID = false)
+             $filter = "objectguid=" . $username;
+         }
+         else if (strstr($username, "@")) {
+-             $filter = "userPrincipalName=" . $username;
++             $filter = "userPrincipalName=" . $this->adldap->utilities()->ldapSlashes($username);
+         }
+         else {
+-             $filter = "samaccountname=" . $username;
++             $filter = "samaccountname=" . $this->adldap->utilities()->ldapSlashes($username);
+         }
+         $filter = "(&(objectCategory=person)({$filter}))";
+         if ($fields === NULL) { 
+
+From ccb7e6fcc548be15c1a71499a7bb16e4d9d08a13 Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 17:43:03 +0200
+Subject: [PATCH 4/8] security(extension): validate link URLs and escape remote
+ data to prevent XSS
+
+The Extension Manager renders metadata fetched from the remote repository
+into an admin-only tool. buildAttributes() escapes metacharacters but does
+not validate URL schemes, so a listing can set a link target to a
+javascript: URL that executes in the superuser's session on click.
+
+Fix: reject non-http(s) URLs before emitting the bugtracker, donation,
+download and repository links. The pluginrepo API stores and serves these
+fields (bugtracker, donationurl, downloadurl, sourcerepo) verbatim, so they
+are attacker-controlled and the javascript: vector is reachable.
+
+Hardening only: the same scheme check on the homepage and screenshot links,
+plus hsc() on the gravatar emailid and the compatible version label/date.
+These are not reachable through the official repository today (the homepage
+url is not served by the API, screenshot urls are wrapped by ml(), emailid
+is an md5 hash, and the compatible label/date come from server config), but
+guarding them keeps a future repository-side change from reopening the gap.
+
+Severity: medium - a javascript: URL supplied through the public extension
+repository API runs in the superuser's session when they click a plugin's
+link in the admin-only Extension Manager.
+
+fixes #4698
+---
+ lib/plugins/extension/GuiExtension.php | 42 +++++++++++++++++---------
+ 1 file changed, 28 insertions(+), 14 deletions(-)
+
+diff --git a/lib/plugins/extension/GuiExtension.php b/lib/plugins/extension/GuiExtension.php
+index f82ed58550..340c0aaca5 100644
+--- a/lib/plugins/extension/GuiExtension.php
++++ b/lib/plugins/extension/GuiExtension.php
+@@ -80,7 +80,7 @@ protected function thumbnail()
+             'alt' => '',
+         ];
+ 
+-        if ($screen) {
++        if ($screen && $this->isValidURL($screen)) {
+             $link = [
+                 'href' => $screen,
+                 'target' => '_blank',
+@@ -169,20 +169,18 @@ public function mainLinks()
+         $html = '<div class="linkbar">';
+ 
+ 
+-        $homepage = $this->extension->getURL();
+-        if ($homepage) {
+-            $params = $this->prepareLinkAttributes($homepage, 'homepage');
++        $params = $this->prepareLinkAttributes($this->extension->getURL(), 'homepage');
++        if ($params) {
+             $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('homepage_link') . '</a>';
+         }
+ 
+-        $bugtracker = $this->extension->getBugtrackerURL();
+-        if ($bugtracker) {
+-            $params = $this->prepareLinkAttributes($bugtracker, 'bugs');
++        $params = $this->prepareLinkAttributes($this->extension->getBugtrackerURL(), 'bugs');
++        if ($params) {
+             $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('bugs_features') . '</a>';
+         }
+ 
+-        if ($this->extension->getDonationURL()) {
+-            $params = $this->prepareLinkAttributes($this->extension->getDonationURL(), 'donate');
++        $params = $this->prepareLinkAttributes($this->extension->getDonationURL(), 'donate');
++        if ($params) {
+             $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('donate_action') . '</a>';
+         }
+ 
+@@ -237,7 +235,7 @@ protected function details()
+ 
+         if (!$this->extension->isBundled() && $this->extension->getCompatibleVersions()) {
+             $list['compatible'] = implode(', ', array_map(
+-                static fn($date, $version) => '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>',
++                static fn($date, $version) => '<bdi>' . hsc($version['label']) . ' (' . hsc($date) . ')</bdi>',
+                 array_keys($this->extension->getCompatibleVersions()),
+                 array_values($this->extension->getCompatibleVersions())
+             ));
+@@ -299,7 +297,7 @@ protected function author()
+         if ($mailid) {
+             $url = $this->tabURL('search', ['q' => 'authorid:' . $mailid]);
+             $html = '<a href="' . $url . '" class="author" title="' . $this->getLang('author_hint') . '" >' .
+-                '<img src="//www.gravatar.com/avatar/' . $mailid .
++                '<img src="//www.gravatar.com/avatar/' . hsc($mailid) .
+                 '?s=60&amp;d=mm" width="20" height="20" alt="" /> ' .
+                 hsc($name) . '</a>';
+         } else {
+@@ -400,6 +398,20 @@ protected function getClasses()
+         return implode(' ', $classes);
+     }
+ 
++    /**
++     * Check whether the given URL is a safe http(s) URL
++     *
++     * Remote repository data must not be able to smuggle in other schemes
++     * such as javascript: which would execute in the admin's session.
++     *
++     * @param string $url The URL to check
++     * @return bool
++     */
++    protected function isValidURL($url)
++    {
++        return (bool)preg_match('#^https?://#i', (string)$url);
++    }
++
+     /**
+      * Create an attributes array for a link
+      *
+@@ -407,12 +419,14 @@ protected function getClasses()
+      *
+      * @param string $url The URL to link to
+      * @param string $class Additional classes to add
+-     * @return array
++     * @return array Empty when the URL is missing or not a safe http(s) URL
+      */
+     protected function prepareLinkAttributes($url, $class)
+     {
+         global $conf;
+ 
++        if (!$this->isValidURL($url)) return [];
++
+         $attributes = [
+             'href' => $url,
+             'class' => 'urlextern',
+@@ -447,7 +461,8 @@ protected function prepareLinkAttributes($url, $class)
+      */
+     protected function shortlink($url, $class, $fallback = '')
+     {
+-        if (!$url) return $fallback;
++        $params = $this->prepareLinkAttributes($url, $class);
++        if (!$params) return $fallback;
+ 
+         $link = parse_url($url);
+         $base = $link['host'];
+@@ -457,7 +472,6 @@ protected function shortlink($url, $class, $fallback = '')
+ 
+         $name = shorten($base, $long, 55);
+ 
+-        $params = $this->prepareLinkAttributes($url, $class);
+         $html = '<a ' . buildAttributes($params, true) . '>' . hsc($name) . '</a>';
+         return $html;
+     }
+
+From bed5e6c509bb30aecd08a03c71df1b2441c6fae6 Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 19:13:36 +0200
+Subject: [PATCH 5/8] security(extension): serve ajax toggle error responses as
+ plain text
+
+The ajax toggle handler echoed error messages that embed the raw ext
+parameter while inheriting the ajax.php default Content-Type of
+text/html. Navigating directly to the ajax URL with a crafted ext value
+could therefore reflect markup into an HTML response.
+
+Default the handler's output to text/plain so error messages are inert;
+the success path still overrides to text/html before rendering HTML.
+
+Severity: low - reflected XSS requiring the target to follow a crafted ajax
+URL; the extension manager is superuser-only, so the reach and audience are
+narrow.
+
+fixes #4699
+---
+ lib/plugins/extension/action.php | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/lib/plugins/extension/action.php b/lib/plugins/extension/action.php
+index 50156dcaf4..5e1618df5e 100644
+--- a/lib/plugins/extension/action.php
++++ b/lib/plugins/extension/action.php
+@@ -40,6 +40,9 @@ public function handleAjaxToggle(Event $event, $param)
+         $event->preventDefault();
+         $event->stopPropagation();
+ 
++        // error responses below are plain text; the success path overrides this before rendering HTML
++        header('Content-Type: text/plain; charset=utf-8');
++
+         /** @var admin_plugin_extension $admin */
+         $admin = plugin_load('admin', 'extension');
+         if (!$admin->isAccessibleByCurrentUser()) {
+
+From 59232b81859c4dbfea628c2e7862ffb9e07c8868 Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 20:29:50 +0200
+Subject: [PATCH 6/8] security(extension): avoid DOM XSS when opening
+ screenshot lightbox
+
+The screenshot lightbox read the repository-supplied screenshot URL back
+out of the link's href and concatenated it into an HTML string, so a URL
+carrying a quote could break out of the src attribute and run script in the
+admin's session on click. Build the img element via jQuery's attribute API
+instead so the URL is never parsed as HTML.
+
+This is hardening, not a fix for a vector reachable through the official
+repository: the pluginrepo API serves screenshoturl as an ml()-wrapped
+dokuwiki.org fetch.php link with the author-supplied value rawurlencode()'d
+into the media parameter, so no HTML metacharacter survives. Landing a
+literal quote in the field requires a tampered API response, i.e. a
+man-in-the-middle on the repository connection or a compromised repository
+backend.
+
+Severity: low - not reachable through the official repository, which encodes
+the value; exploitation needs a man-in-the-middle or a compromised repository
+backend to supply a crafted screenshot URL, plus an admin who clicks the
+thumbnail.
+
+fixes #4700
+---
+ lib/plugins/extension/script.js | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/lib/plugins/extension/script.js b/lib/plugins/extension/script.js
+index 61301d1f9d..8212aaf488 100644
+--- a/lib/plugins/extension/script.js
++++ b/lib/plugins/extension/script.js
+@@ -39,7 +39,7 @@ jQuery(function () {
+         // fill and show it
+         $lightbox
+             .show()
+-            .find('div').html('<img src="' + image_href + '" />');
++            .find('div').empty().append(jQuery('<img>', {src: image_href}));
+ 
+         return false;
+     });
+
+From 3ed34089439ec60adc294b4d4b976c5c88cf5be5 Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 20:40:16 +0200
+Subject: [PATCH 7/8] security(auth): use strict comparison in membership and
+ ACL checks
+
+Loose comparison treated numerically-equal strings as the same value, so
+a superuser, manager, or ACL entry named with a numeric string (e.g. 1e3
+or @1e3) matched any user or group whose name was numerically equal
+(1000, 01000, 1000.0). Such names survive auth_nameencode() untouched,
+allowing silent privilege escalation. Match names strictly in
+auth_isMember() and auth_aclcheck_cb().
+
+Severity: low - only exploitable when an administrator names a superuser,
+manager or ACL group with a numeric string such as 1e3, an unusual
+configuration, so realistic deployments are rarely affected.
+
+fixes #4701
+---
+ _test/tests/inc/auth_admincheck.test.php | 18 ++++++++++++++++++
+ inc/auth.php                             |  8 ++++----
+ 2 files changed, 22 insertions(+), 4 deletions(-)
+
+diff --git a/inc/auth.php b/inc/auth.php
+index c1ac770a53..665d101f28 100644
+--- a/inc/auth.php
++++ b/inc/auth.php
+@@ -670,10 +670,10 @@ function auth_isMember($memberlist, $user, array $groups)
+         if (!$auth->isCaseSensitive()) $member = PhpString::strtolower($member);
+         if ($member[0] == '@') {
+             $member = $auth->cleanGroup(substr($member, 1));
+-            if (in_array($member, $groups)) return true;
++            if (in_array($member, $groups, true)) return true;
+         } else {
+             $member = $auth->cleanUser($member);
+-            if ($member == $user) return true;
++            if ($member === $user) return true;
+         }
+     }
+ 
+@@ -803,7 +803,7 @@ function auth_aclcheck_cb($data)
+             if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
+                 $acl[1] = PhpString::strtolower($acl[1]);
+             }
+-            if (!in_array($acl[1], $groups)) {
++            if (!in_array($acl[1], $groups, true)) {
+                 continue;
+             }
+             if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
+@@ -833,7 +833,7 @@ function auth_aclcheck_cb($data)
+                 if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
+                     $acl[1] = PhpString::strtolower($acl[1]);
+                 }
+-                if (!in_array($acl[1], $groups)) {
++                if (!in_array($acl[1], $groups, true)) {
+                     continue;
+                 }
+                 if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
+
+From 0579c2f8a6cf8c76cb756b91a4ab1167dcffd4af Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <andi@splitbrain.org>
+Date: Sun, 19 Jul 2026 22:33:30 +0200
+Subject: [PATCH 8/8] security(media): authorize the resolved target of media
+ write operations
+
+The media manager computed a permission level for the request's namespace
+and passed it to the media write helpers, which then acted on a target that
+need not live in that namespace. Reachability differs per helper:
+
+- media_restore() took the id to restore from the separate "image"
+  parameter, unrelated to the namespace the permission was computed for. A
+  user with upload permission in any one namespace could restore revisions
+  of media in namespaces they cannot write.
+
+- media_upload() and media_upload_xhr() derive the target from the upload
+  namespace plus a user-controlled filename that may carry its own namespace
+  segments, so the target can only resolve into a sub-namespace of the upload
+  namespace. Where that child has a stricter ACL, the parent's permission was
+  applied instead.
+
+- media_metasave() was not vulnerable: its target and its permission both
+  derive from the same request id, so the namespaces always matched. It is
+  changed only for consistency.
+
+Each helper now recomputes the permission from its resolved target id, as
+media_delete() already does. The redundant $auth parameter is removed from
+media_restore() and media_metasave(), which have no other callers, and
+ignored but kept for compatibility in media_upload() and media_upload_xhr().
+
+Severity: medium - an authenticated user with upload rights in one namespace
+can restore media revisions in other namespaces, or write into a stricter
+sub-namespace of one they can upload to; an authorization bypass affecting
+media integrity, not code execution or disclosure.
+
+fixes #4702
+---
+ _test/tests/inc/media_metasave.test.php |  50 ++++++++
+ _test/tests/inc/media_restore.test.php  |  77 ++++++++++++
+ _test/tests/inc/media_upload.test.php   | 154 ++++++++++++++++++++++++
+ inc/media.php                           |  36 +++---
+ lib/exe/mediamanager.php                |   6 +-
+ 5 files changed, 304 insertions(+), 19 deletions(-)
+ create mode 100644 _test/tests/inc/media_metasave.test.php
+ create mode 100644 _test/tests/inc/media_restore.test.php
+ create mode 100644 _test/tests/inc/media_upload.test.php
+
+diff --git a/inc/media.php b/inc/media.php
+index 63d06e2311..e30ffcdd05 100644
+--- a/inc/media.php
++++ b/inc/media.php
+@@ -64,13 +64,12 @@ function media_filesinuse($data, $id)
+  * @author Kate Arzamastseva <pshns@ukr.net>
+  *
+  * @param string $id media id
+- * @param int $auth permission level
+  * @param array $data
+  * @return false|string
+  */
+-function media_metasave($id, $auth, $data)
++function media_metasave($id, $data)
+ {
+-    if ($auth < AUTH_UPLOAD) return false;
++    if (auth_quickaclcheck(mediaAclPath($id)) < AUTH_UPLOAD) return false;
+     if (!checkSecurityToken()) return false;
+     global $lang;
+     global $conf;
+@@ -305,11 +304,12 @@ function media_delete($id, $auth)
+ /**
+  * Handle file uploads via XMLHttpRequest
+  *
+- * @param string $ns   target namespace
+- * @param int    $auth current auth check result
++ * @param string   $ns   target namespace
++ * @param null|int $auth deprecated and ignored, the ACL is re-checked against
++ *                       the resolved target id
+  * @return false|string false on error, id of the new file on success
+  */
+-function media_upload_xhr($ns, $auth)
++function media_upload_xhr($ns, $auth = null)
+ {
+     if (!checkSecurityToken()) return false;
+     global $INPUT;
+@@ -328,11 +328,12 @@ function media_upload_xhr($ns, $auth)
+         return false;
+     }
+ 
++    $target = cleanID($ns . ':' . $id);
+     $res = media_save(
+         ['name' => $path, 'mime' => $mime, 'ext'  => $ext],
+-        $ns . ':' . $id,
++        $target,
+         ($INPUT->get->str('ow') == 'true'),
+-        $auth,
++        auth_quickaclcheck(mediaAclPath($target)),
+         'copy'
+     );
+     unlink($path);
+@@ -351,11 +352,12 @@ function media_upload_xhr($ns, $auth)
+  * @author Michael Klier <chi@chimeric.de>
+  *
+  * @param string     $ns    target namespace
+- * @param int        $auth  current auth check result
++ * @param null|int   $auth  deprecated and ignored, the ACL is re-checked
++ *                          against the resolved target id
+  * @param bool|array $file  $_FILES member, $_FILES['upload'] if false
+  * @return false|string false on error, id of the new file on success
+  */
+-function media_upload($ns, $auth, $file = false)
++function media_upload($ns, $auth = null, $file = false)
+ {
+     if (!checkSecurityToken()) return false;
+     global $lang;
+@@ -381,15 +383,16 @@ function media_upload($ns, $auth, $file = false)
+         msg(sprintf($lang['mediaextchange'], $fext, $iext));
+     }
+ 
++    $target = cleanID($ns . ':' . $id);
+     $res = media_save(
+         [
+             'name' => $file['tmp_name'],
+             'mime' => $imime,
+             'ext' => $iext
+         ],
+-        $ns . ':' . $id,
++        $target,
+         $INPUT->post->bool('ow'),
+-        $auth,
++        auth_quickaclcheck(mediaAclPath($target)),
+         'copy_uploaded_file'
+     );
+     if (is_array($res)) {
+@@ -1300,15 +1303,16 @@ function media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $type)
+  *
+  * @param string $image media id
+  * @param int    $rev   revision timestamp or empty string
+- * @param int    $auth
+- * @return string - file's id
++ * @return false|string the file's id, or false on error
+  *
+  * @author Kate Arzamastseva <pshns@ukr.net>
+  */
+-function media_restore($image, $rev, $auth)
++function media_restore($image, $rev)
+ {
+     global $conf;
+-    if ($auth < AUTH_UPLOAD || !$conf['mediarevisions']) return false;
++    if (!$conf['mediarevisions']) return false;
++    $image = cleanID($image);
++    if (auth_quickaclcheck(mediaAclPath($image)) < AUTH_UPLOAD) return false;
+     $removed = (!file_exists(mediaFN($image)) && file_exists(mediaMetaFN($image, '.changes')));
+     if (!$image || (!file_exists(mediaFN($image)) && !$removed)) return false;
+     if (!$rev || !file_exists(mediaFN($image, $rev))) return false;
+diff --git a/lib/exe/mediamanager.php b/lib/exe/mediamanager.php
+index 3e509e73cf..6e10c31e3d 100644
+--- a/lib/exe/mediamanager.php
++++ b/lib/exe/mediamanager.php
+@@ -86,17 +86,17 @@
+ 
+ // handle meta saving
+ if ($IMG && @array_key_exists('save', $INPUT->arr('do'))) {
+-    $JUMPTO = media_metasave($IMG, $AUTH, $INPUT->arr('meta'));
++    $JUMPTO = media_metasave($IMG, $INPUT->arr('meta'));
+ }
+ 
+ if ($IMG && ($INPUT->str('mediado') == 'save' || @array_key_exists('save', $INPUT->arr('mediado')))) {
+-    $JUMPTO = media_metasave($IMG, $AUTH, $INPUT->arr('meta'));
++    $JUMPTO = media_metasave($IMG, $INPUT->arr('meta'));
+ }
+ 
+ if ($INPUT->int('rev') && $conf['mediarevisions']) $REV = $INPUT->int('rev');
+ 
+ if ($INPUT->str('mediado') == 'restore' && $conf['mediarevisions'] && checkSecurityToken()) {
+-    $JUMPTO = media_restore($INPUT->str('image'), $REV, $AUTH);
++    $JUMPTO = media_restore($INPUT->str('image'), $REV);
+ }
+ 
+ // handle deletion

diff --git a/CVE-2026-37106.patch b/CVE-2026-37106.patch
new file mode 100644
index 0000000..11e1e5c
--- /dev/null
+++ b/CVE-2026-37106.patch
@@ -0,0 +1,40 @@
+From ed28f990b6e1705dab522ad34afa8c02b188dc91 Mon Sep 17 00:00:00 2001
+From: Andreas Gohr <gohr@cosmocode.de>
+Date: Wed, 1 Jul 2026 14:51:51 +0200
+Subject: [PATCH] use actionOK() to gate the register action
+
+The Register action duplicated the openregister and addUser capability
+checks that actionOK('register') already performs. Delegate to it so the
+action gate and register() itself share a single source of truth.
+
+See https://github.com/dokuwiki/dokuwiki/issues/4682#issuecomment-4854987500
+---
+ inc/Action/Register.php | 8 ++------
+ 1 file changed, 2 insertions(+), 6 deletions(-)
+
+diff --git a/inc/Action/Register.php b/inc/Action/Register.php
+index 232d21a015..33e0596f7d 100644
+--- a/inc/Action/Register.php
++++ b/inc/Action/Register.php
+@@ -5,7 +5,6 @@
+ use dokuwiki\Ui\UserRegister;
+ use dokuwiki\Action\Exception\ActionAbort;
+ use dokuwiki\Action\Exception\ActionDisabledException;
+-use dokuwiki\Extension\AuthPlugin;
+ use dokuwiki\Ui;
+ 
+ /**
+@@ -28,11 +27,8 @@ public function checkPreconditions()
+     {
+         parent::checkPreconditions();
+ 
+-        /** @var AuthPlugin $auth */
+-        global $auth;
+-        global $conf;
+-        if (isset($conf['openregister']) && !$conf['openregister']) throw new ActionDisabledException();
+-        if (!$auth->canDo('addUser')) throw new ActionDisabledException();
++        // actionOK() bundles the disableactions, legacy openregister and addUser capability checks
++        if (!actionOK('register')) throw new ActionDisabledException();
+     }
+ 
+     /** @inheritdoc */

diff --git a/dokuwiki.spec b/dokuwiki.spec
index 2488fa9..5de5fc3 100644
--- a/dokuwiki.spec
+++ b/dokuwiki.spec
@@ -5,7 +5,7 @@ License:	GPL-2.0-only
 %global		releasenum 2025-05-14b
 %global		releasetag %(rel="%{releasenum}"; echo "${rel//-/}")
 Version:	%{releasetag}
-Release:	3%{?dist}
+Release:	4%{?dist}
 
 %global php_min_version 7.4
 
@@ -19,6 +19,17 @@ Patch1:		dokuwiki-rm-bundled-libs.patch
 # https://github.com/dokuwiki/dokuwiki/commit/bfc167db63967f8c872b3d797ca81138b9011ef4
 Patch2:		CVE-2026-26477.patch
 
+# Upstream considers the CVE to be bogus, but still used it as opportunity
+# to remove some obsolete code used in critical paths.
+#
+# Backport from upstream:
+# https://github.com/dokuwiki/dokuwiki/commit/ed28f990b6e1705dab522ad34afa8c02b188dc91
+Patch3:		CVE-2026-37106.patch
+
+# Backport from upstream. Edited to remove changes to tests.
+# https://github.com/dokuwiki/dokuwiki/pull/4703
+Patch4:		4703.patch
+
 BuildArch:	noarch
 
 %global smoke_test 1
@@ -280,6 +291,9 @@ fi
 %doc DOKUWIKI-SELINUX.README
 
 %changelog
+* Wed Jul 22 2026 Artur Frenszek-Iwicki <fedora@svgames.pl> - 20250514b-4
+- Backport some more security patches
+
 * Mon Apr 20 2026 Artur Frenszek-Iwicki <fedora@svgames.pl> - 20250514b-3
 - Add a patch for CVE-2026-26477
 

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-07-22 20:17 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-22 20:17 [rpms/dokuwiki] f43: Backport some more security patches Artur Frenszek-Iwicki

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox