{"record":{"id":"e75f76bc7f9d598c","repo":"BigPizzaV3/CodexPlusPlus","slug":"zip-entry-escapes-destination-name","errorCode":null,"errorMessage":"zip entry escapes destination: {name}","messagePattern":"zip entry escapes destination: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/codex-plus-core/src/plugin_marketplace.rs","lineNumber":449,"sourceCode":"                .with_context(|| format!(\"failed to create {}\", parent.display()))?;\n        }\n        let mut contents = Vec::new();\n        file.read_to_end(&mut contents)\n            .with_context(|| format!(\"failed to read zip entry {}\", file.name()))?;\n        std::fs::write(&output_path, contents)\n            .with_context(|| format!(\"failed to write {}\", output_path.display()))?;\n    }\n    Ok(())\n}\n\nfn safe_zip_path(name: &str) -> anyhow::Result<PathBuf> {\n    let path = Path::new(name);\n    let mut relative = PathBuf::new();\n    for component in path.components() {\n        match component {\n            Component::Normal(value) => relative.push(value),\n            Component::CurDir => {}\n            _ => anyhow::bail!(\"zip entry escapes destination: {name}\"),\n        }\n    }\n    if relative.as_os_str().is_empty() {\n        anyhow::bail!(\"zip entry has empty path\");\n    }\n    Ok(relative)\n}\n\nfn zip_entry_relative_path(name: &str) -> Option<PathBuf> {\n    let path = Path::new(name);\n    let mut components = path.components();\n    match components.next()? {\n        Component::Normal(_) => {}\n        _ => return None,\n    }\n    let mut relative = PathBuf::new();\n    for component in components {\n        match component {","sourceCodeStart":431,"sourceCodeEnd":467,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/plugin_marketplace.rs#L431-L467","documentation":"safe_zip_path (crates/codex-plus-core/src/plugin_marketplace.rs:442) sanitizes every entry name before extraction of the marketplace zip: it keeps only Component::Normal parts, collapses CurDir ('.'), and rejects anything else — ParentDir ('..'), root separators ('/'), and Windows prefixes ('C:\\') — with 'zip entry escapes destination: {name}'. This is a zip-slip defense: without it, an entry like ../../.ssh/authorized_keys would write outside the destination directory.","triggerScenarios":"Installing the openai/plugins marketplace zip (install path calls safe_zip_path at plugin_marketplace.rs:422) when any entry name contains '..' or an absolute/prefixed path — i.e. a maliciously crafted zip, a zip built with absolute paths, or one packed by a tool that emits leading slashes.","commonSituations":"Supply-chain attack or tampered artifact where the zip tries to escape; zips created with `zip -r /abs/path` style absolute names; test fixtures hand-writing entry names with '..'; a compromised or misconfigured CDN serving a doctored bundle.","solutions":["Do not whitelist or patch the sanitizer — treat the error as the artifact being untrusted: re-download from the official OPENAI_PLUGINS_ZIP_URL over a clean network path and verify the artifact source","Inspect the zip locally: `unzip -l bundle.zip | grep -E '\\.\\.|^/'` to list offending entry names and confirm tampering vs. tooling artifact","If you produce the zip yourself (internal marketplace mirror), repack with relative paths only (cd into the root and zip from there)","Keep the embedded marketplace copy as fallback and report the bad artifact upstream"],"exampleFix":"# before: packing with absolute roots produces escaping entries\ncd / && zip -r /tmp/marketplace.zip /home/user/.agents/plugins  # entries like home/user/...\n\n# after: pack from inside the root so all components are Normal\ncd ~/.agents/plugins && zip -r /tmp/marketplace.zip .","handlingStrategy":"validation","validationCode":"// Scan entries before extraction (mirror of safe_zip_path)\nfn zip_is_safe(names: impl IntoIterator<Item = String>) -> bool {\n    names.into_iter().all(|name| {\n        std::path::Path::new(&name).components().all(|c| matches!(c,\n            std::path::Component::Normal(_) | std::path::Component::CurDir))\n    })\n}\nassert!(zip_is_safe(zip.file_names().map(str::to_string)));","typeGuard":"fn entry_is_safe(name: &str) -> bool {\n    !name.contains(\"..\") && !name.starts_with('/') && !name.contains(\":\\\\\")\n        && !name.replace('\\\\', \"/\").split('/').any(|p| p == \"..\")\n}","tryCatchPattern":"// Never bypass: a hit means untrusted artifact\nmatch install_openai_plugins_zip(home, &bytes) {\n    Err(e) if e.to_string().contains(\"escapes destination\") => {\n        tracing::error!(\"zip-slip attempt detected; discarding download\");\n        quarantine_download(&bytes); // keep for forensics, do not install\n        return Err(e.context(\"untrusted marketplace artifact\"));\n    }\n    rest => rest?,\n}","preventionTips":["Always extract through safe_zip_path (or equivalent component-whitelist) — never raw names","Download only from the pinned official OPENAI_PLUGINS_ZIP_URL over trusted network paths","When producing zips, pack from inside the root so all components are relative and Normal","Quarantine and report artifacts that attempt traversal instead of retrying them"],"tags":["rust","zip","zip-slip","path-traversal","security","plugin-marketplace"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}