{"record":{"id":"b14ad2bcc2dda788","repo":"Hmbown/CodeWhale","slug":"plugin-archive-bundle-path-must-contain-only-safe-relative","errorCode":null,"errorMessage":"plugin archive bundle path must contain only safe relative directory names","messagePattern":"plugin archive bundle path must contain only safe relative directory names","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/plugins/install/mod.rs","lineNumber":165,"sourceCode":"    };\n    let url = reqwest::Url::parse(raw).context(\"invalid plugin archive URL\")?;\n    let Some(fragment) = url.fragment() else {\n        return Ok(None);\n    };\n    let path = fragment\n        .strip_prefix(\"path=\")\n        .context(\"plugin archive fragment must be #path=<bundle-directory>\")?;\n    if path.is_empty()\n        || !path.split('/').all(|part| {\n            !part.is_empty()\n                && part != \".\"\n                && part != \"..\"\n                && part\n                    .bytes()\n                    .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, b'-' | b'_' | b'.'))\n        })\n    {\n        bail!(\"plugin archive bundle path must contain only safe relative directory names\");\n    }\n    Ok(Some(path.to_string()))\n}\n\n/// Serialize a source for the `.installed-from` marker. Must round-trip\n/// through [`PluginInstallSource::parse`].\nfn plugin_spec_string(source: &PluginInstallSource, canonical_source: Option<&Path>) -> String {\n    match source {\n        PluginInstallSource::LocalPath(_) => {\n            let path = canonical_source.expect(\"local installs record the canonical source\");\n            format!(\"path:{}\", path.display())\n        }\n        PluginInstallSource::Remote(remote) => source_spec_string(remote),\n    }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Outcome / result types","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/plugins/install/mod.rs#L147-L183","documentation":"`remote_bundle_path` validates the bundle path inside a plugin archive tarball before extracting it. Every path component must be a plain relative directory/file name of ASCII alphanumerics, `-`, `_`, or `.` only — no absolute paths, `..` traversal, separators, or other characters. The library bails here to prevent archive members from escaping the install directory (zip-slip).","triggerScenarios":"Installing a remote plugin archive whose internal bundle path contains an absolute component (leading `/`), a `..` component, an empty component, or characters outside `[A-Za-z0-9._-]` (e.g. spaces, unicode, `+`). Raised from `parse` / `install_remote_bytes` when processing the downloaded tarball.","commonSituations":"A plugin author packaged the tarball with nested folders like `my plugin/dist` or `./build/../plugin`; a build tool emitted absolute member paths; a malicious or misconfigured archive contains traversal entries.","solutions":["Repackage the archive so the bundle sits under a flat relative directory using only [A-Za-z0-9._-] in every component","Verify member paths with `tar -tf bundle.tar.gz` and fix any absolute or `..` entries","If the source is your own CI artifact, change the packaging step (e.g. `tar -czf bundle.tar.gz -C dist .`)","If you do not control the archive, download and inspect it manually instead of using remote install"],"exampleFix":"// before: members like \"/abs/path/plugin.wasm\" or \"../plugin.wasm\"\ntar -czf bundle.tar.gz ./plugin\n// after: flat, relative, safe names\ntar -czf bundle.tar.gz -C dist plugin.wasm","handlingStrategy":"validation","validationCode":"fn is_safe_bundle_path(path: &str) -> bool {\n    !path.starts_with('/')\n        && path.split('/').all(|p| {\n            !p.is_empty() && p != \".\" && p != \"..\"\n                && p.bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.'))\n        })\n}\n// call before handing the tarball to install_remote_bytes","typeGuard":null,"tryCatchPattern":"match install_remote_bytes(bytes) {\n    Err(e) if e.to_string().contains(\"safe relative directory names\") => {\n        eprintln!(\"archive layout rejected; repackage with flat relative paths\")\n    }\n    other => other?,\n}","preventionTips":["Inspect tarball members (`tar -tf`) before publishing","Package with `-C dist .` so paths stay relative and flat","Never include absolute paths or `..` in archive entries"],"tags":["plugins","path-validation","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}