CVE-2026-42795
Description
Symlink following vulnerability in Gleam's Hex package export allows files outside the project root to be embedded in the generated package tarball.
The file collection helpers (gleam_files, native_files, private_files) in compiler-cli/src/fs.rs use follow_links(true) when walking publishable directories such as src/ and priv/. The collected paths are added to the package archive via add_path_to_tar in compiler-cli/src/publish.rs without verifying that the resolved target remains within the project root. A symlink placed under a publishable directory will cause gleam export hex-tarball or gleam publish to embed the contents of the symlink target into the generated Hex package.
An attacker with write access to the project repository can place a symlink in src/ or priv/ pointing to an arbitrary file. When a maintainer or CI pipeline runs gleam publish or gleam export hex-tarball, local files readable by the publisher (such as secrets, tokens, or SSH keys) are silently embedded into the published package artifact.
This issue affects Gleam from 0.10.0-rc1 until 1.17.0.
Affected products
1- Range: 0.10.0-rc1 to 1.17.0
Patches
16435a5528b9aCorrect tar path handling
2 files changed · +26 −15
compiler-cli/src/publish.rs+21 −15 modified@@ -565,10 +565,10 @@ fn do_build_hex_tarball(paths: &ProjectPaths, config: &mut PackageConfig) -> Res let mut tarball = Vec::new(); { let mut tarball = tar::Builder::new(&mut tarball); - add_to_tar(&mut tarball, "VERSION", version.as_bytes())?; - add_to_tar(&mut tarball, "metadata.config", metadata.as_bytes())?; - add_to_tar(&mut tarball, "contents.tar.gz", contents_tar_gz.as_slice())?; - add_to_tar(&mut tarball, "CHECKSUM", checksum.as_bytes())?; + add_to_tar_from_memory(&mut tarball, "VERSION", version.as_bytes())?; + add_to_tar_from_memory(&mut tarball, "metadata.config", metadata.as_bytes())?; + add_to_tar_from_memory(&mut tarball, "contents.tar.gz", contents_tar_gz.as_slice())?; + add_to_tar_from_memory(&mut tarball, "CHECKSUM", checksum.as_bytes())?; tarball.finish().map_err(Error::finish_tar)?; } tracing::info!("Generated package Hex release tarball"); @@ -657,10 +657,10 @@ fn contents_tarball( let mut tarball = tar::Builder::new(GzEncoder::new(&mut contents_tar_gz, Compression::default())); for path in files { - add_path_to_tar(&mut tarball, paths, path)?; + add_to_tar_from_file_system(&mut tarball, paths, path)?; } for (path, contents) in data_files { - add_to_tar(&mut tarball, path, contents.as_bytes())?; + add_to_tar_from_memory(&mut tarball, path, contents.as_bytes())?; } tarball.finish().map_err(Error::finish_tar)?; } @@ -740,13 +740,14 @@ fn generated_erlang_files( Ok(files) } -fn add_to_tar<P, W>(tarball: &mut tar::Builder<W>, path: P, data: &[u8]) -> Result<()> +fn add_to_tar_from_memory<P, W>(tarball: &mut tar::Builder<W>, path: P, data: &[u8]) -> Result<()> where P: AsRef<Utf8Path>, W: Write, { let path = path.as_ref(); - tracing::info!(file=?path, "Adding file to tarball"); + tracing::info!(file=?path, "Adding in memory file to tarball"); + let mut header = tar::Header::new_gnu(); header.set_mode(0o600); header.set_size(data.len() as u64); @@ -756,22 +757,27 @@ where .map_err(|e| Error::add_tar(path, e)) } -fn add_path_to_tar<P, W>(tarball: &mut tar::Builder<W>, paths: &ProjectPaths, path: P) -> Result<()> +fn add_to_tar_from_file_system<P, W>( + tarball: &mut tar::Builder<W>, + paths: &ProjectPaths, + path: P, +) -> Result<()> where P: AsRef<Utf8Path>, W: Write, { let path = path.as_ref(); + tracing::info!(file=?&path, "Adding file system file to tarball"); + let path = fs::canonicalise(path)?; - if !path.starts_with(paths.root()) { + let Ok(path) = path.strip_prefix(paths.root()) else { return Err(Error::TarPathOutsideOfProjectRoot { path }); - } + }; - tracing::info!(file=?&path, "Adding file to tarball"); tarball - .append_path(&path) - .map_err(|e| Error::add_tar(&path, e)) + .append_path(path) + .map_err(|e| Error::add_tar(path, e)) } #[test] @@ -787,7 +793,7 @@ fn add_to_tar_symlink_rejection_test() { // not be possible to add it to the tar archive. let outside_path = path.join("outside.txt"); std::fs::write(&outside_path, "Hello").unwrap(); - match add_path_to_tar(&mut tarball, &paths, &outside_path).unwrap_err() { + match add_to_tar_from_file_system(&mut tarball, &paths, &outside_path).unwrap_err() { Error::TarPathOutsideOfProjectRoot { path } => assert_eq!(path, outside_path), other => panic!("Unexpected error {other:?}"), }
test/hextarball/src/external_module.erl+5 −0 added@@ -0,0 +1,5 @@ +-module(external_module). +-export([hello/0]). + +hello() -> + "Hello!".
Vulnerability mechanics
Root cause
"The file collection helpers did not verify that symlink targets remained within the project root."
Attack vector
An attacker with write access to a project repository can place a symbolic link within a publishable directory (such as `src/` or `priv/`) that points to an arbitrary file on the system [ref_id=1]. When a maintainer or CI pipeline executes `gleam publish` or `gleam export hex-tarball`, the contents of the symlink target are silently embedded into the generated package artifact [ref_id=4]. This allows sensitive local files, like secrets or SSH keys, to be exfiltrated into the published package [ref_id=1].
Affected code
The vulnerability lies within the file collection helpers `gleam_files`, `native_files`, and `private_files` in `compiler-cli/src/fs.rs`, which use `follow_links(true)` [ref_id=1]. These collected paths are then processed by `add_path_to_tar` in `compiler-cli/src/publish.rs` [ref_id=1]. The patch modifies `compiler-cli/src/publish.rs` by introducing `add_to_tar_from_file_system` which now checks if the canonicalized path is within the project root [patch_id=4494236].
What the fix does
The patch renames `add_path_to_tar` to `add_to_tar_from_file_system` and modifies it to use `path.strip_prefix(paths.root())` [patch_id=4494236]. This ensures that only paths strictly within the project root are appended to the tarball, preventing the inclusion of files outside the project directory even if they are reached via symlinks. The function `add_to_tar` was also renamed to `add_to_tar_from_memory` to distinguish its purpose [patch_id=4494236].
Preconditions
- inputAttacker must have write access to the project repository to place a symlink.
Reproduction
In a Gleam project, create a symlink inside `src/` pointing to a sensitive file: `ln -s ~/.ssh/id_rsa src/steal.mjs`. Run `gleam export hex-tarball` (or `gleam publish`). Inspect the tarball: the contents of `~/.ssh/id_rsa` are present under `src/steal.mjs` in the archive [ref_id=4].
Generated on Jun 2, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.