Description: Subversion servers reveal 'copyfrom' paths that should be hidden
 according to configured path-based authorization (authz) rules.  When a node
 has been copied from a protected location, users with access to the copy can
 see the 'copyfrom' path of the original.  This also reveals the fact that the
 node was copied.  Only the 'copyfrom' path is revealed; not its contents. Both
 httpd and svnserve servers are vulnerable.
Author: Stefan Sperling <stsp@apache.org>
Origin: upstream
Last-Update: 2022-04-04
---
This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
--- a/subversion/libsvn_repos/log.c
+++ b/subversion/libsvn_repos/log.c
@@ -337,42 +337,36 @@ detect_changed(svn_repos_revision_access
       if (   (change->change_kind == svn_fs_path_change_add)
           || (change->change_kind == svn_fs_path_change_replace))
         {
-          const char *copyfrom_path = change->copyfrom_path;
-          svn_revnum_t copyfrom_rev = change->copyfrom_rev;
-
           /* the following is a potentially expensive operation since on FSFS
              we will follow the DAG from ROOT to PATH and that requires
              actually reading the directories along the way. */
           if (!change->copyfrom_known)
             {
-              SVN_ERR(svn_fs_copied_from(&copyfrom_rev, &copyfrom_path,
+              SVN_ERR(svn_fs_copied_from(&change->copyfrom_rev, &change->copyfrom_path,
                                         root, path, iterpool));
               change->copyfrom_known = TRUE;
             }
 
-          if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev))
+          if (change->copyfrom_path && SVN_IS_VALID_REVNUM(change->copyfrom_rev))
             {
-              svn_boolean_t readable = TRUE;
-
               if (callbacks->authz_read_func)
                 {
                   svn_fs_root_t *copyfrom_root;
+                  svn_boolean_t readable;
 
                   SVN_ERR(svn_fs_revision_root(&copyfrom_root, fs,
-                                               copyfrom_rev, iterpool));
+                                               change->copyfrom_rev, iterpool));
                   SVN_ERR(callbacks->authz_read_func(&readable,
                                                      copyfrom_root,
-                                                     copyfrom_path,
+                                                     change->copyfrom_path,
                                                      callbacks->authz_read_baton,
                                                      iterpool));
                   if (! readable)
-                    found_unreadable = TRUE;
-                }
-
-              if (readable)
-                {
-                  change->copyfrom_path = copyfrom_path;
-                  change->copyfrom_rev = copyfrom_rev;
+                    {
+                      found_unreadable = TRUE;
+                      change->copyfrom_path = NULL;
+                      change->copyfrom_rev = SVN_INVALID_REVNUM;
+                    }
                 }
             }
         }
--- subversion-1.13.0.orig/subversion/tests/cmdline/authz_tests.py
+++ subversion-1.13.0/subversion/tests/cmdline/authz_tests.py
@@ -1524,6 +1524,61 @@ def authz_del_from_subdir(sbox):
                                       'rm', sbox.repo_url + '/A/mu',
                                       '-m', '')
 
+# test for the bug also known as CVE-2021-28544
+@Skip(svntest.main.is_ra_type_file)
+def log_inaccessible_copyfrom(sbox):
+  "log doesn't leak inaccessible copyfrom paths"
+
+  sbox.build(empty=True)
+  sbox.simple_add_text('secret', 'private')
+  sbox.simple_commit(message='log message for r1')
+  sbox.simple_copy('private', 'public')
+  sbox.simple_commit(message='log message for r2')
+
+  svntest.actions.enable_revprop_changes(sbox.repo_dir)
+  # Remove svn:date and svn:author for predictable output.
+  svntest.actions.run_and_verify_svn(None, [], 'propdel', '--revprop',
+                                     '-r2', 'svn:date', sbox.repo_url)
+  svntest.actions.run_and_verify_svn(None, [], 'propdel', '--revprop',
+                                     '-r2', 'svn:author', sbox.repo_url)
+
+  write_restrictive_svnserve_conf(sbox.repo_dir)
+
+  # First test with blanket access.
+  write_authz_file(sbox,
+                   {"/" : "* = rw"})
+  expected_output = svntest.verify.ExpectedOutput([
+    "------------------------------------------------------------------------\n",
+    "r2 | (no author) | (no date) | 1 line\n",
+    "Changed paths:\n",
+    "   A /public (from /private:1)\n",
+    "\n",
+    "log message for r2\n",
+    "------------------------------------------------------------------------\n",
+  ])
+  svntest.actions.run_and_verify_svn(expected_output, [],
+                                     'log', '-r2', '-v',
+                                     sbox.repo_url)
+
+  # Now test with an inaccessible copy source (/private).
+  write_authz_file(sbox,
+                   {"/" : "* = rw"},
+                   {"/private" : "* ="})
+  expected_output = svntest.verify.ExpectedOutput([
+    "------------------------------------------------------------------------\n",
+    "r2 | (no author) | (no date) | 1 line\n",
+    "Changed paths:\n",
+    # The copy is shown as a plain add with no copyfrom info.
+    "   A /public\n",
+    "\n",
+    # No log message, as the revision is only partially visible.
+    "\n",
+    "------------------------------------------------------------------------\n",
+  ])
+  svntest.actions.run_and_verify_svn(expected_output, [],
+                                     'log', '-r2', '-v',
+                                     sbox.repo_url)
+
 
 @SkipUnless(svntest.main.is_ra_type_dav) # dontdothat is dav only
 def log_diff_dontdothat(sbox):
@@ -1771,6 +1826,7 @@ test_list = [ None,
               inverted_group_membership,
               group_member_empty_string,
               empty_group,
+              log_inaccessible_copyfrom,
              ]
 serial_only = True
 
