CVE-2026-43965
Description
Path traversal vulnerability in Gleam's dependency management allows arbitrary directory deletion via malicious build/packages/packages.toml content.
Package keys read from build/packages/packages.toml by LocalPackages::read_from_disc are passed without validation to paths.build_packages_package(), which constructs a filesystem path by joining the project build directory with the attacker-controlled key. The resulting path is then passed to fs::delete_directory (which calls remove_dir_all). No check is performed to ensure the path remains within the intended build/packages/ directory. Both absolute paths and relative traversal sequences (e.g. ../) are accepted as package keys, allowing deletion of arbitrary directories.
An attacker who can cause a victim to run gleam deps download on a project containing a malicious build/packages/packages.toml (e.g. by committing the normally-gitignored file to a repository) can cause arbitrary directories on the victim's system to be recursively deleted.
This issue affects Gleam from 0.18.0-rc1 until 1.17.0.
Affected products
1- Range: >=0.18.0-rc1 <=1.17.0
Patches
1690ca069817bRestrict packages.toml deserialisation
5 files changed · +60 −16
CHANGELOG.md+7 −2 modified@@ -517,8 +517,13 @@ ([evipepota](https://github.com/evipepota) and ([Louis Pilfold](https://github.com/lpil)) -- Restrict publication tarball creation so they cannot contain files from - outside the project root. +- Stricter deserialisation rules for files internal the build directory to + reject corrupted data. ([Abdelrahman Ahmed Aboelkasem](https://github.com/0x2face), ([Aly](https://github.com/spect3r1), and ([Louis Pilfold](https://github.com/lpil)) + +- Restrict publication tarball creation so they cannot contain files from + outside the project root. + ([Abdelrahman Ahmed Aboelkasem](https://github.com/0x2face), + ([Aly](https://github.com/spect3r1), and ([Louis Pilfold](https://github.com/lpil))
compiler-cli/src/dependencies.rs+36 −4 modified@@ -571,13 +571,14 @@ fn write_manifest_to_disc(paths: &ProjectPaths, manifest: &Manifest) -> Result<( // the `project/build/packages` directory. // For descriptions of packages provided by paths and git deps, see the ProvidedPackage struct. // The same package may appear in both at different times. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] struct LocalPackages { - packages: HashMap<String, Version>, + #[serde(deserialize_with = "gleam_core::config::map_with_package_name_keys::deserialize")] + packages: HashMap<EcoString, Version>, } impl LocalPackages { - pub fn extra_local_packages(&self, manifest: &Manifest) -> Vec<(String, Version)> { + pub fn extra_local_packages(&self, manifest: &Manifest) -> Vec<(EcoString, Version)> { let manifest_packages: HashSet<_> = manifest .packages .iter() @@ -634,12 +635,43 @@ impl LocalPackages { packages: manifest .packages .iter() - .map(|p| (p.name.to_string(), p.version.clone())) + .map(|p| (p.name.clone(), p.version.clone())) .collect(), } } } +#[test] +fn local_packages_deserialise_ok() { + let toml = r#" +[packages] +gleam_stdlib = "1.0.0" +gleam_otp = "1.1.0" +"#; + let packages: LocalPackages = toml::from_str(toml).unwrap(); + assert_eq!( + packages, + LocalPackages { + packages: HashMap::from_iter([ + ("gleam_stdlib".into(), Version::new(1, 0, 0)), + ("gleam_otp".into(), Version::new(1, 1, 0)), + ]) + } + ) +} + +#[test] +fn local_packages_deserialise_invalid_name() { + let toml = r#" +[packages] +gleam_stdlib = "1.0.0" +"../../stuff" = "1.1.0" +"#; + let error = toml::from_str::<LocalPackages>(toml) + .expect_err("should fail to deserialise because of invalid name"); + insta::assert_snapshot!(insta::internals::AutoName, error.to_string()); +} + fn is_same_requirements( requirements1: &HashMap<EcoString, Requirement>, requirements2: &HashMap<EcoString, Requirement>,
compiler-cli/src/snapshots/gleam_cli__dependencies__local_packages_deserialise_invalid_name.snap+9 −0 added@@ -0,0 +1,9 @@ +--- +source: compiler-cli/src/dependencies.rs +expression: error.to_string() +--- +TOML parse error at line 2, column 1 + | +2 | [packages] + | ^^^^^^^^^^ +invalid value: string "../../stuff", expected a package name containing only lowercase letter, numbers, and underscores
compiler-core/src/config.rs+7 −9 modified@@ -34,8 +34,6 @@ fn default_javascript_runtime() -> Runtime { Runtime::NodeJs } -pub type Dependencies = HashMap<EcoString, Requirement>; - #[derive(Clone, Debug, PartialEq, Eq)] pub struct SpdxLicense { pub licence: String, @@ -166,16 +164,16 @@ pub struct PackageConfig { #[serde( default, serialize_with = "ordered_map", - deserialize_with = "dependencies_map::deserialize" + deserialize_with = "map_with_package_name_keys ::deserialize" )] - pub dependencies: Dependencies, + pub dependencies: HashMap<EcoString, Requirement>, #[serde( default, alias = "dev-dependencies", serialize_with = "ordered_map", - deserialize_with = "dependencies_map::deserialize" + deserialize_with = "map_with_package_name_keys ::deserialize" )] - pub dev_dependencies: Dependencies, + pub dev_dependencies: HashMap<EcoString, Requirement>, #[serde(default)] pub repository: Option<Repository>, #[serde(default)] @@ -217,7 +215,7 @@ where } impl PackageConfig { - pub fn dependencies_for(&self, mode: Mode) -> Result<Dependencies> { + pub fn dependencies_for(&self, mode: Mode) -> Result<HashMap<EcoString, Requirement>> { match mode { Mode::Dev | Mode::Lsp => self.all_direct_dependencies(), Mode::Prod => Ok(self.dependencies.clone()), @@ -226,7 +224,7 @@ impl PackageConfig { // Return all the dependencies listed in the configuration, that is, all the // direct dependencies, both in the `dependencies` and `dev_dependencies`. - pub fn all_direct_dependencies(&self) -> Result<Dependencies> { + pub fn all_direct_dependencies(&self) -> Result<HashMap<EcoString, Requirement>> { let mut deps = HashMap::with_capacity(self.dependencies.len() + self.dev_dependencies.len()); for (name, requirement) in self.dependencies.iter().chain(&self.dev_dependencies) { @@ -1207,7 +1205,7 @@ pub(crate) mod package_name { } } -pub(crate) mod dependencies_map { +pub mod map_with_package_name_keys { use ecow::EcoString; use serde::{Deserialize, Deserializer, de}; use std::collections::HashMap;
compiler-core/src/manifest.rs+1 −1 modified@@ -13,7 +13,7 @@ use itertools::Itertools; pub struct Manifest { #[serde( serialize_with = "ordered_map", - deserialize_with = "super::config::dependencies_map::deserialize" + deserialize_with = "super::config::map_with_package_name_keys ::deserialize" )] pub requirements: HashMap<EcoString, Requirement>, #[serde(serialize_with = "sorted_vec")]
Vulnerability mechanics
Root cause
"Package keys from build/packages/packages.toml are not validated before being used to construct filesystem paths for deletion."
Attack vector
An attacker can create a malicious `build/packages/packages.toml` file with package keys containing relative traversal sequences (e.g., `../`) or absolute paths. By tricking a victim into running `gleam deps download` on a project containing this malicious file, the attacker can cause arbitrary directories on the victim's system to be recursively deleted [ref_id=1]. This is possible because the `build/packages/packages.toml` file is normally gitignored, but can be committed to a repository [ref_id=4].
Affected code
The vulnerability lies within the `LocalPackages::read_from_disc` function in `compiler-cli/src/dependencies.rs`. This function reads package keys from `build/packages/packages.toml` and passes them without validation to `paths.build_packages_package()`, which then uses them to construct filesystem paths for deletion via `fs::delete_directory` [ref_id=1]. The deserialization logic in `compiler-core/src/config.rs` was also modified.
What the fix does
The patch introduces stricter deserialization rules for package names within `build/packages/packages.toml` [patch_id=4494234]. Specifically, the `map_with_package_name_keys::deserialize` function is now used, which validates that package names only contain allowed characters and do not represent traversal sequences or absolute paths. This prevents the construction of malicious filesystem paths that could lead to arbitrary directory deletion [ref_id=3].
Preconditions
- inputA malicious `build/packages/packages.toml` file with crafted package keys.
- inputThe victim must run `gleam deps download` on a project containing the malicious file.
Reproduction
[packages] "/tmp/gleam-delete-me" = "1.0.0"
Run `gleam deps download` in the project directory. `/tmp/gleam-delete-me` is recursively deleted. A `../../../some/path` traversal variant achieves the same effect. [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.