Skip to content

Commit 53b3f2f

Browse files
committed
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.
1 parent 216eee6 commit 53b3f2f

1 file changed

Lines changed: 21 additions & 4 deletions

File tree

src/scripts/jefferson

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import stat
66
import os
77
import zlib
88
import binascii
9-
109
import cstruct
1110

1211
from jefferson import jffs2_lzma, rtime
@@ -52,6 +51,12 @@ JFFS2_NODETYPE_XREF = JFFS2_FEATURE_INCOMPAT | JFFS2_NODE_ACCURATE | 9
5251
def mtd_crc(data):
5352
return (binascii.crc32(data, -1) ^ -1) & 0xFFFFFFFF
5453

54+
def is_safe_path(basedir, path, follow_symlinks=True):
55+
if follow_symlinks:
56+
matchpath = os.path.realpath(path)
57+
else:
58+
matchpath = os.path.abspath(path)
59+
return basedir == os.path.commonpath((basedir, matchpath))
5560

5661
cstruct.typedef("uint8", "uint8_t")
5762
cstruct.typedef("uint16", "jint16_t")
@@ -474,20 +479,32 @@ def dump_fs(fs, target):
474479
node_names.append(dirent.name.decode())
475480
path = "/".join(node_names)
476481

477-
target_path = os.path.join(os.getcwd(), target, path)
482+
target_path = os.path.realpath(os.path.join(os.getcwd(), target, path))
483+
484+
if not is_safe_path(target, target_path):
485+
print(f"Path traversal attempt to {target_path}, discarding.")
486+
continue
487+
478488
for inode in dirent.inodes:
479489
try:
480490
if stat.S_ISDIR(inode.mode):
481491
print("writing S_ISDIR", path)
482492
if not os.path.isdir(target_path):
483493
os.makedirs(target_path)
484494
elif stat.S_ISLNK(inode.mode):
495+
link_path = inode.data.decode('utf-8')
496+
if link_path:
497+
link_path = link_path[1:]
498+
link_target = os.path.realpath(os.path.join(target, link_path))
499+
if not is_safe_path(target, link_target):
500+
print(f"Path traversal attempt through symlink to {link_target}, discarding.")
501+
continue
485502
print("writing S_ISLNK", path)
486503
if not os.path.islink(target_path):
487504
if os.path.exists(target_path):
488505
print("file already exists as", inode.data)
489506
continue
490-
os.symlink(inode.data, target_path)
507+
os.symlink(link_target, target_path)
491508
elif stat.S_ISREG(inode.mode):
492509
print("writing S_ISREG", path)
493510
if not os.path.isfile(target_path):
@@ -561,7 +578,7 @@ def main():
561578
if not fs[JFFS2_NODETYPE_DIRENT]:
562579
continue
563580

564-
dest_path_fs = os.path.join(dest_path, "fs_%i" % fs_index)
581+
dest_path_fs = os.path.realpath(os.path.join(dest_path, "fs_%i" % fs_index))
565582
print("dumping fs #%i to %s" % (fs_index, dest_path_fs))
566583
for key, value in fs.items():
567584
if key == "endianness":

0 commit comments

Comments
 (0)