{"record":{"id":"0d09cd25a271db74","repo":"tonhowtf/omniget","slug":"plugin-id-must-not-be-a-relative-path-component-plugin-id","errorCode":null,"errorMessage":"plugin id must not be a relative path component: {plugin_id:?}","messagePattern":"plugin id must not be a relative path component: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/src/plugin_loader.rs","lineNumber":236,"sourceCode":"    /// caminho arbitrario.\n    pub fn plugin_dir(&self, plugin_id: &str) -> anyhow::Result<PathBuf> {\n        validate_plugin_id(plugin_id)?;\n        Ok(self.plugins_dir.join(plugin_id))\n    }\n}\n\n/// Aceita apenas `[A-Za-z0-9._-]`, recusando separador de caminho, byte nulo,\n/// componente relativo e id vazio.\n///\n/// Allowlist e nao blocklist de proposito: o conjunto de ids reais e pequeno e\n/// conhecido (`courses`, `study`, `telegram`, `convert`, `misc`), e uma\n/// blocklist erra por omissao a cada codificacao nova.\npub fn validate_plugin_id(plugin_id: &str) -> anyhow::Result<()> {\n    if plugin_id.is_empty() {\n        anyhow::bail!(\"plugin id must not be empty\");\n    }\n    if plugin_id == \".\" || plugin_id == \"..\" || plugin_id.starts_with('.') {\n        anyhow::bail!(\"plugin id must not be a relative path component: {plugin_id:?}\");\n    }\n    if let Some(bad) = plugin_id\n        .chars()\n        .find(|c| !c.is_ascii_alphanumeric() && !matches!(c, '.' | '_' | '-'))\n    {\n        anyhow::bail!(\"plugin id contains an illegal character {bad:?}: {plugin_id:?}\");\n    }\n    if plugin_id.contains(\"..\") {\n        anyhow::bail!(\"plugin id must not contain '..': {plugin_id:?}\");\n    }\n    Ok(())\n}\n\nfn load_single_plugin(\n    plugin_dir: &Path,\n    host: Arc<dyn PluginHost>,\n) -> Result<LoadedPlugin, PluginLoadError> {\n    let manifest_path = plugin_dir.join(\"plugin.json\");","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/src/plugin_loader.rs#L218-L254","documentation":"`validate_plugin_id` rejects ids equal to \".\" or \"..\" or starting with '.', because such ids are relative path components and could escape or alias the plugins root when building plugin_dir. It bails via anyhow with the offending id in the message.","triggerScenarios":"Passing \".\", \"..\", or an id like \".hidden\" to plugin_dir/validate_plugin_id — typically from a manifest id field or untrusted user input intended as a directory name.","commonSituations":"User-supplied id from a URL/query used directly as a directory name; attacker-crafted id attempting path traversal; typo'd id like \"..backup\".","solutions":["Use ids that do not start with '.' and are not '.'/'..'","Sanitize or reject user-supplied ids before calling plugin_dir","Adopt the allowlist approach noted in the code (courses, study, telegram, convert, misc)"],"exampleFix":"// before\nlet dir = plugin_dir(\"..\")?; // bails\n// after\nensure!(!id.starts_with('.') && id != \"..\", \"invalid plugin id\");\nlet dir = plugin_dir(&id)?;","handlingStrategy":"validation","validationCode":"ensure!(!plugin_id.is_empty() && !plugin_id.starts_with('.') && plugin_id != \"..\", \"invalid plugin id: {plugin_id:?}\");","typeGuard":"fn is_valid_id_shape(id: &str) -> bool { !id.is_empty() && !id.starts_with('.') }","tryCatchPattern":"let dir = plugin_dir(&id).with_context(|| format!(\"bad plugin id {id:?}\"))?;","preventionTips":["Never use raw user input as directory names","Prefer an id allowlist","Sanitize ids before path construction"],"tags":["rust","anyhow","plugin","path-traversal","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8600b91f4246848bac346874daa9e61c1fc5677a","analyzedAt":"2026-09-12T14:29:19.317Z","contentChangedAt":"2026-09-12T14:29:19.317Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}