CVE-2022-4885: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: A vulnerability has been found in sviehb jefferson up to 0.3 and classified as critical. This vulnerability affects unknown code of the file src/scripts/jefferson. The manipulation leads to path traversal. The attack can be initiated remotely. The complexity of an attack is rather high. The exploitation appears to be difficult. Upgrading to version 0.4 is able to address this issue. The name of the patch is 53b3f2fc34af0bb32afbcee29d18213e61471d87. It is recommended to upgrade the affected component. The identifier of this vulnerability is VDB-218020.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 5.9
Published: 2023-01-11T18:15Z
Repository: https://github.com/sviehb/jefferson
Language: Other
Files changed: src/scripts/jefferson
Commit date: 2022-01-24 12:40:31 +0100
Commit message: Fix path traversal security vulnerability by canonicalizing path names of every inodes and discarding inodes with a path
pointing outside of the extraction directory.

Fix path traversal through symlinks by canonicalizing link target path using the extraction directory as root. If the link
still points outside the extraction root, it gets discarded. This way symlinks reflects the reality of a filesystem on device
by pointing to files within the extracted filesystem instead of files from the host executing jefferson.

Diff: diff --git a/src/scripts/jefferson b/src/scripts/jefferson
index d598c5f..7a5f059 100755
--- a/src/scripts/jefferson
+++ b/src/scripts/jefferson
@@ -4,11 +4,10 @@ import argparse
 import struct
 import stat
 import os
 import zlib
 import binascii
-
 import cstruct
 
 from jefferson import jffs2_lzma, rtime
 
 
@@ -50,10 +49,16 @@ JFFS2_NODETYPE_XREF = JFFS2_FEATURE_INCOMPAT | JFFS2_NODE_ACCURATE | 9
 
 
 def mtd_crc(data):
     return (binascii.crc32(data, -1) ^ -1) & 0xFFFFFFFF
 
+def is_safe_path(basedir, path, follow_symlinks=True):
+    if follow_symlinks:
+        matchpath = os.path.realpath(path)
+    else:
+        matchpath = os.path.abspath(path)
+    return basedir == os.path.commonpath((basedir, matchpath))
 
 cstruct.typedef("uint8", "uint8_t")
 cstruct.typedef("uint16", "jint16_t")
 cstruct.typedef("uint32", "jint32_t")
 cstruct.typedef("uint32", "jmode_t")
@@ -472,24 +477,36 @@ def dump_fs(fs, target):
         for pnode in pnodes:
             node_names.append(pnode.name.decode())
         node_names.append(dirent.name.decode())
         path = "/".join(node_names)
 
-        target_path = os.path.join(os.getcwd(), target, path)
+        target_path = os.path.realpath(os.path.join(os.getcwd(), target, path))
+
+        if not is_safe_path(target, target_path):
+            print(f"Path traversal attempt to {target_path}, discarding.")
+            continue
+
         for inode in dirent.inodes:
             try:
                 if stat.S_ISDIR(inode.mode):
                     print("writing S_ISDIR", path)
                     if not os.path.isdir(target_path):
                         os.makedirs(target_path)
                 elif stat.S_ISLNK(inode.mode):
+                    link_path = inode.data.decode('utf-8')
+                    if link_path:
+                        link_path = link_path[1:]
+                    link_target = os.path.realpath(os.path.join(target, link_path))
+                    if not is_safe_path(target, link_target):
+                        print(f"Path traversal attempt through symlink to {link_target}, discarding.")
+                        continue
                     print("writing S_ISLNK", path)
                     if not os.path.islink(target_path):
                         if os.path.exists(target_path):
                             print("file already exists as", inode.data)
                             continue
-                        os.symlink(inode.data, target_path)
+                        os.symlink(link_target, target_path)
                 elif stat.S_ISREG(inode.mode):
                     print("writing S_ISREG", path)
                     if not os.path.isfile(target_path):
                         if not os.path.isdir(os.path.dirname(target_path)):
                             os.makedirs(os.path.dirname(target_path))
@@ -559,11 +576,11 @@ def main():
     fs_index = 1
     for fs in fs_list:
         if not fs[JFFS2_NODETYPE_DIRENT]:
             continue
 
-        dest_path_fs = os.path.join(dest_path, "fs_%i" % fs_index)
+        dest_path_fs = os.path.realpath(os.path.join(dest_path, "fs_%i" % fs_index))
         print("dumping fs #%i to %s" % (fs_index, dest_path_fs))
         for key, value in fs.items():
             if key == "endianness":
                 if value == cstruct.BIG_ENDIAN:
                     print("Endianness: Big")

Vulnerable code: 
        target_path = os.path.join(os.getcwd(), target, path)
                        os.symlink(inode.data, target_path)
        dest_path_fs = os.path.join(dest_path, "fs_%i" % fs_index)
Fixed code: def is_safe_path(basedir, path, follow_symlinks=True):
    if follow_symlinks:
        matchpath = os.path.realpath(path)
    else:
        matchpath = os.path.abspath(path)
    return basedir == os.path.commonpath((basedir, matchpath))
        target_path = os.path.realpath(os.path.join(os.getcwd(), target, path))

        if not is_safe_path(target, target_path):
            print(f"Path traversal attempt to {target_path}, discarding.")
            continue

                    link_path = inode.data.decode('utf-8')
                    if link_path:
                        link_path = link_path[1:]
                    link_target = os.path.realpath(os.path.join(target, link_path))
                    if not is_safe_path(target, link_target):
                        print(f"Path traversal attempt through symlink to {link_target}, discarding.")
                        continue
                        os.symlink(link_target, target_path)
        dest_path_fs = os.path.realpath(os.path.join(dest_path, "fs_%i" % fs_index))