Disallow writing symlinks outside the source distribution target directory (#12259)

## Summary

Closes #12163.

## Test Plan

Created an offending source distribution with this script:

```python
import io
import tarfile
import textwrap
import time

PKG_NAME  = "badpkg"
VERSION   = "0.1"
DIST_NAME = f"{PKG_NAME}-{VERSION}"
ARCHIVE   = f"{DIST_NAME}.tar.gz"


def _bytes(data: str) -> io.BytesIO:
    """Helper: wrap a text blob as a BytesIO for tarfile.addfile()."""
    return io.BytesIO(data.encode())


def main(out_path: str = ARCHIVE) -> None:
    now = int(time.time())

    with tarfile.open(out_path, mode="w:gz") as tar:

        def add_file(path: str, data: str, mode: int = 0o644) -> None:
            """Add a regular file whose *content* is supplied as a string."""
            buf  = _bytes(data)
            info = tarfile.TarInfo(path)
            info.size   = len(buf.getbuffer())
            info.mtime  = now
            info.mode   = mode
            tar.addfile(info, buf)

        # ── top‑level setup.py ───────────────────────────────────────────────
        setup_py = textwrap.dedent(f"""\
            from setuptools import setup, find_packages
            setup(
                name="{PKG_NAME}",
                version="{VERSION}",
                packages=find_packages(),
            )
        """)
        add_file(f"{DIST_NAME}/setup.py", setup_py)

        # ── minimal package code ─────────────────────────────────────────────
        add_file(f"{DIST_NAME}/{PKG_NAME}/__init__.py", "# placeholder\\n")

        # ── the malicious symlink ────────────────────────────────────────────
        link = tarfile.TarInfo(f"{DIST_NAME}/{PKG_NAME}/evil_link")
        link.type     = tarfile.SYMTYPE
        link.mtime    = now
        link.mode     = 0o777
        link.linkname = "../../../outside.txt"
        tar.addfile(link)

    print(f"Created {out_path}")


if __name__ == "__main__":
    main()
```

Verified that both `pip install` and `uv pip install` rejected it.

I also changed `link.linkname = "../../../outside.txt"` to
`link.linkname = "/etc/outside"`, and verified that the absolute path
was rejected too.
This commit is contained in:
Charlie Marsh 2025-07-22 09:20:09 -04:00 committed by GitHub
parent c8486da495
commit 2677e85df9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 28 additions and 23 deletions

40
Cargo.lock generated
View File

@ -500,22 +500,20 @@ dependencies = [
[[package]] [[package]]
name = "bzip2" name = "bzip2"
version = "0.5.0" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bafdbf26611df8c14810e268ddceda071c297570a5fb360ceddf617fe417ef58" checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
dependencies = [ dependencies = [
"bzip2-sys", "bzip2-sys",
"libc",
] ]
[[package]] [[package]]
name = "bzip2-sys" name = "bzip2-sys"
version = "0.1.11+1.0.8" version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [ dependencies = [
"cc", "cc",
"libc",
"pkg-config", "pkg-config",
] ]
@ -690,9 +688,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]] [[package]]
name = "codspeed" name = "codspeed"
version = "3.0.3" version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7524e02ff6173bc143d9abc01b518711b77addb60de871bbe5686843f88fb48" checksum = "d29180405ab3b37bb020246ea66bf8ae233708766fd59581ae929feaef10ce91"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -708,9 +706,9 @@ dependencies = [
[[package]] [[package]]
name = "codspeed-criterion-compat" name = "codspeed-criterion-compat"
version = "3.0.3" version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f71662331c4f854131a42b95055f3f8cbca53640348985f699635b1f96d8c26" checksum = "2454d874ca820ffd71273565530ad318f413195bbc99dce6c958ca07db362c63"
dependencies = [ dependencies = [
"codspeed", "codspeed",
"codspeed-criterion-compat-walltime", "codspeed-criterion-compat-walltime",
@ -719,9 +717,9 @@ dependencies = [
[[package]] [[package]]
name = "codspeed-criterion-compat-walltime" name = "codspeed-criterion-compat-walltime"
version = "3.0.3" version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c9bd9e895e0aa263d139a8b5f58a4ea4abb86d5982ec7f58d3c7b8465c1e01" checksum = "093a9383cdd1a5a0bd1a47cdafb49ae0c6dcd0793c8fb8f79768bab423128c9c"
dependencies = [ dependencies = [
"anes", "anes",
"cast", "cast",
@ -761,7 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -1593,11 +1591,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.11" version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3347,7 +3345,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.9.2", "linux-raw-sys 0.9.2",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -6234,9 +6232,9 @@ dependencies = [
[[package]] [[package]]
name = "wasmtimer" name = "wasmtimer"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" checksum = "d8d49b5d6c64e8558d9b1b065014426f35c18de636895d24893dbbd329743446"
dependencies = [ dependencies = [
"futures", "futures",
"js-sys", "js-sys",
@ -6341,7 +6339,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -6972,7 +6970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e9a772a54b54236b9b744aaaf8d7be01b4d6e99725523cb82cb32d1c81b1d7" checksum = "84e9a772a54b54236b9b744aaaf8d7be01b4d6e99725523cb82cb32d1c81b1d7"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"bzip2 0.5.0", "bzip2 0.5.2",
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"displaydoc", "displaydoc",

View File

@ -76,7 +76,7 @@ anstream = { version = "0.6.15" }
anyhow = { version = "1.0.89" } anyhow = { version = "1.0.89" }
arcstr = { version = "1.2.0" } arcstr = { version = "1.2.0" }
arrayvec = { version = "0.7.6" } arrayvec = { version = "0.7.6" }
astral-tokio-tar = { version = "0.5.1" } astral-tokio-tar = { version = "0.5.2" }
async-channel = { version = "2.3.1" } async-channel = { version = "2.3.1" }
async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] } async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] }
async-trait = { version = "0.1.82" } async-trait = { version = "0.1.82" }

View File

@ -236,6 +236,7 @@ pub async fn untar_gz<R: tokio::io::AsyncRead + Unpin>(
) )
.set_preserve_mtime(false) .set_preserve_mtime(false)
.set_preserve_permissions(false) .set_preserve_permissions(false)
.set_allow_external_symlinks(false)
.build(); .build();
Ok(untar_in(archive, target.as_ref()).await?) Ok(untar_in(archive, target.as_ref()).await?)
} }
@ -255,6 +256,7 @@ pub async fn untar_bz2<R: tokio::io::AsyncRead + Unpin>(
) )
.set_preserve_mtime(false) .set_preserve_mtime(false)
.set_preserve_permissions(false) .set_preserve_permissions(false)
.set_allow_external_symlinks(false)
.build(); .build();
Ok(untar_in(archive, target.as_ref()).await?) Ok(untar_in(archive, target.as_ref()).await?)
} }
@ -274,6 +276,7 @@ pub async fn untar_zst<R: tokio::io::AsyncRead + Unpin>(
) )
.set_preserve_mtime(false) .set_preserve_mtime(false)
.set_preserve_permissions(false) .set_preserve_permissions(false)
.set_allow_external_symlinks(false)
.build(); .build();
Ok(untar_in(archive, target.as_ref()).await?) Ok(untar_in(archive, target.as_ref()).await?)
} }
@ -293,6 +296,7 @@ pub async fn untar_xz<R: tokio::io::AsyncRead + Unpin>(
) )
.set_preserve_mtime(false) .set_preserve_mtime(false)
.set_preserve_permissions(false) .set_preserve_permissions(false)
.set_allow_external_symlinks(false)
.build(); .build();
untar_in(archive, target.as_ref()).await?; untar_in(archive, target.as_ref()).await?;
Ok(()) Ok(())
@ -311,6 +315,7 @@ pub async fn untar<R: tokio::io::AsyncRead + Unpin>(
tokio_tar::ArchiveBuilder::new(&mut reader as &mut (dyn tokio::io::AsyncRead + Unpin)) tokio_tar::ArchiveBuilder::new(&mut reader as &mut (dyn tokio::io::AsyncRead + Unpin))
.set_preserve_mtime(false) .set_preserve_mtime(false)
.set_preserve_permissions(false) .set_preserve_permissions(false)
.set_allow_external_symlinks(false)
.build(); .build();
untar_in(archive, target.as_ref()).await?; untar_in(archive, target.as_ref()).await?;
Ok(()) Ok(())

View File

@ -1754,13 +1754,14 @@ fn build_with_symlink() -> Result<()> {
build-backend = "hatchling.build" build-backend = "hatchling.build"
"#})?; "#})?;
fs_err::os::unix::fs::symlink( fs_err::os::unix::fs::symlink(
context.temp_dir.child("pyproject.toml.real"), "pyproject.toml.real",
context.temp_dir.child("pyproject.toml"), context.temp_dir.child("pyproject.toml"),
)?; )?;
context context
.temp_dir .temp_dir
.child("src/softlinked/__init__.py") .child("src/softlinked/__init__.py")
.touch()?; .touch()?;
fs_err::remove_dir_all(&context.venv)?;
uv_snapshot!(context.filters(), context.build(), @r###" uv_snapshot!(context.filters(), context.build(), @r###"
success: true success: true
exit_code: 0 exit_code: 0
@ -1799,6 +1800,7 @@ fn build_with_hardlink() -> Result<()> {
.temp_dir .temp_dir
.child("src/hardlinked/__init__.py") .child("src/hardlinked/__init__.py")
.touch()?; .touch()?;
fs_err::remove_dir_all(&context.venv)?;
uv_snapshot!(context.filters(), context.build(), @r###" uv_snapshot!(context.filters(), context.build(), @r###"
success: true success: true
exit_code: 0 exit_code: 0