CVE-2018-10897 (published 2018-08-01T17:29Z)

A directory traversal issue was found in reposync, a part of yum-utils, where reposync fails to sanitize paths in remote repository configuration files. If an attacker controls a repository, they may be able to copy files outside of the destination directory on the targeted system via path traversal. If reposync is running with heightened privileges on a targeted system, this flaw could potentially result in system compromise via the overwriting of critical system files. Version 1.1.31 and older are believed to be affected.

Severity: HIGH
CVSS v2 base score: 9.3 (AV:N/AC:M/Au:N/C:C/I:C/A:C)
CVSS v3 base score: 8.1, HIGH (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-59
cwe_name: Improper Link Resolution Before File Access ('Link Following')
description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/rpm-software-management/yum-utils
commit_details: repo_url: https://github.com/rpm-software-management/yum-utils
author: Michal Domonkos
author_date: 2018-07-19 15:17:03+02:00
msg: reposync: refactor: sanitize remote_path usage

For better readability (remote_path == relativepath).
repository: repo_url: https://github.com/rpm-software-management/yum-utils
repo_name: rpm-software-management/yum-utils
description: [DEPRECATED] A collection of utilities, plugins and examples related to the YUM package manager
repo_language: Python
owner: rpm-software-management
file_changes: filename: reposync.py
change_type: ModificationType.MODIFY
diff: @@ -221,8 +221,7 @@ def main():
 
             download_set = {}
             for pkg in download_list:
-                remote = pkg.returnSimple('relativepath')
-                rpmname = os.path.basename(remote)
+                rpmname = os.path.basename(pkg.remote_path)
                 download_set[rpmname] = 1
 
             for pkg in current_pkgs:
@@ -269,8 +268,7 @@ def main():
         remote_size = 0
         if not opts.urls:
             for pkg in download_list:
-                remote = pkg.returnSimple('relativepath')
-                local = local_repo_path + '/' + remote
+                local = os.path.join(local_repo_path, pkg.remote_path)
                 sz = int(pkg.returnSimple('packagesize'))
                 if os.path.exists(local) and os.path.getsize(local) == sz:
                     continue
@@ -282,10 +280,9 @@ def main():
         download_list.sort(key=lambda pkg: pkg.name)
         if opts.urls:
             for pkg in download_list:
-                remote = pkg.returnSimple('relativepath')
-                local = os.path.join(local_repo_path, remote)
+                local = os.path.join(local_repo_path, pkg.remote_path)
                 if not (os.path.exists(local) and my.verifyPkg(local, pkg, False)):
-                    print urljoin(pkg.repo.urls[0], pkg.relativepath)
+                    print urljoin(pkg.repo.urls[0], pkg.remote_path)
             continue
 
         # create dest dir
@@ -294,8 +291,7 @@ def main():
 
         # set localpaths
         for pkg in download_list:
-            rpmfn = pkg.remote_path
-            pkg.localpath = os.path.join(local_repo_path, rpmfn)
+            pkg.localpath = os.path.join(local_repo_path, pkg.remote_path)
             pkg.repo.copy_local = True
             pkg.repo.cache = 0
             localdir = os.path.dirname(pkg.localpath)

programming_language: Python
repo_url: https://github.com/rpm-software-management/yum-utils
commit_details: repo_url: https://github.com/rpm-software-management/yum-utils
author: Michal Domonkos
author_date: 2018-07-19 15:17:37+02:00
msg: reposync: prevent path traversal. BZ 1552328
repository: repo_url: https://github.com/rpm-software-management/yum-utils
repo_name: rpm-software-management/yum-utils
description: [DEPRECATED] A collection of utilities, plugins and examples related to the YUM package manager
repo_language: Python
owner: rpm-software-management
file_changes: filename: reposync.1
change_type: ModificationType.MODIFY
diff: @@ -47,6 +47,16 @@ Download all the non-default metadata
 Download only newest packages per-repo.
 .IP "\fB\-q, \-\-quiet\fP"
 Output as little information as possible.
+.IP "\fB\-\-allow-path-traversal\fP"
+Allow packages stored outside their repo directory to be synced.
+These are packages that are referenced in metadata by using absolute paths or
+up-level ".." symbols, and are normally skipped by \fBreposync\fR for security
+reasons.
+
+\fBCAUTION:\fR Using this option has potential security implications since, by
+providing malicious repodata, an attacker could make \fBreposync\fR write to
+arbitrary locations on the file system that are accessible by the user running
+it.
 .SH "EXAMPLES"
 .IP "Sync all packages from the 'updates' repo to the current directory:"
 \fB reposync \-\-repoid=updates\fP

programming_language: Shell
filename: reposync.py
change_type: ModificationType.MODIFY
diff: @@ -74,6 +74,12 @@ def localpkgs(directory):
             cache[name] = {'path': fn, 'size': st.st_size, 'device': st.st_dev}
     return cache
 
+def is_subpath(path, root):
+    root = os.path.realpath(root)
+    path = os.path.realpath(os.path.join(root, path))
+    # join() is used below to ensure root ends with a slash
+    return path.startswith(os.path.join(root, ''))
+
 def parseArgs():
     usage = _("""
     Reposync is used to synchronize a remote yum repository to a local 
@@ -116,6 +122,10 @@ def parseArgs():
     parser.add_option("", "--download-metadata", dest="downloadmd",
         default=False, action="store_true",
         help=_("download all the non-default metadata"))
+    parser.add_option("", "--allow-path-traversal", default=False,
+        action="store_true",
+        help=_("Allow packages stored outside their repo directory to be synced "
+               "(UNSAFE, USE WITH CAUTION!)"))
     (opts, args) = parser.parse_args()
     return (opts, args)
 
@@ -216,6 +226,30 @@ def main():
         else:
             local_repo_path = opts.destdir + '/' + repo.id
 
+        # Ensure we don't traverse out of local_repo_path by dropping any
+        # packages whose remote_path is absolute or contains up-level
+        # references (unless explicitly allowed).
+        # See RHBZ#1600221 for details.
+        if not opts.allow_path_traversal:
+            newlist = []
+            skipped = False
+            for pkg in download_list:
+                if is_subpath(pkg.remote_path, local_repo_path):
+                    newlist.append(pkg)
+                    continue
+                my.logger.warning(
+                    _('WARNING: skipping package %s: remote path "%s" not '
+                      'within repodir, unsafe to mirror locally')
+                    % (pkg, pkg.remote_path)
+                )
+                skipped = True
+            if skipped:
+                my.logger.info(
+                    _('You can enable unsafe remote paths by using '
+                      '--allow-path-traversal (see reposync(1) for details)')
+                )
+            download_list = newlist
+
         if opts.delete and os.path.exists(local_repo_path):
             current_pkgs = localpkgs(local_repo_path)
 

programming_language: Python