{"record":{"id":"e95803fc6a6719e6","repo":"BigPizzaV3/CodexPlusPlus","slug":"refusing-to-delete-script-outside-user-script-dire","errorCode":null,"errorMessage":"refusing to delete script outside user script directory","messagePattern":"refusing to delete script outside user script directory","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/user_scripts.rs","lineNumber":139,"sourceCode":"        let canonical_user_dir = self\n            .user_dir\n            .canonicalize()\n            .or_else(|_| {\n                fs::create_dir_all(&self.user_dir)?;\n                self.user_dir.canonicalize()\n            })\n            .with_context(|| {\n                format!(\n                    \"failed to resolve user script directory {}\",\n                    self.user_dir.display()\n                )\n            })?;\n        if path.exists() {\n            let canonical_path = path\n                .canonicalize()\n                .with_context(|| format!(\"failed to resolve user script {}\", path.display()))?;\n            if !canonical_path.starts_with(&canonical_user_dir) {\n                anyhow::bail!(\"refusing to delete script outside user script directory\");\n            }\n            fs::remove_file(&canonical_path).with_context(|| {\n                format!(\"failed to delete user script {}\", canonical_path.display())\n            })?;\n        }\n\n        let _guard = self.config_lock.lock().unwrap();\n        let mut config = self.load_config_unlocked();\n        config.scripts.remove(key);\n        config.market.remove(key);\n        self.save_config_unlocked(&config)?;\n        Ok(config)\n    }\n\n    pub fn user_script_path_for_market_id(&self, id: &str) -> PathBuf {\n        self.user_dir.join(market_script_filename(id))\n    }\n","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/user_scripts.rs#L121-L157","documentation":"The final filesystem guard in delete_user_script: if the target exists, both the file and the user scripts directory are canonicalized (symlinks resolved), and the file's canonical path must start_with the canonical directory's path; otherwise it bails with \"refusing to delete script outside user script directory\". This defeats symlink attacks — a link placed inside user_dir but pointing elsewhere must not cause deletion of the link target.","triggerScenarios":"A file inside the user scripts directory is a symlink to a file elsewhere (e.g. ln -s ~/.bashrc <user_dir>/x.sh) and delete_user_script(\"user:x.sh\") is called; canonicalize() resolves the link, the target lies outside canonical_user_dir, and the delete is refused. Also fires in the race window if the entry is swapped for a symlink between the exists() check and canonicalization.","commonSituations":"Users symlinking shared scripts into the scripts dir instead of copying; dotfile-manager cross-links; security tests verifying the guard; TOCTOU attempts.","solutions":["Replace symlinks in the user scripts directory with real copies of the scripts","If deleting the symlink itself is the intent, remove it manually (rm <user_dir>/x.sh) — the library deliberately refuses","Audit how the symlink got there if you did not create it; treat it as a possible tampering signal"],"exampleFix":"# instead of a symlink inside the scripts dir\ncp /path/to/shared/tool.js ~/.codex-plus/user-scripts/tool.js\n\n# then the manager delete works:\n# manager.delete_user_script(\"user:tool.js\")","handlingStrategy":"try-catch","validationCode":"let name = key.strip_prefix(\"user:\").unwrap_or(key);\nlet path = user_dir.join(name);\nif path.is_symlink() {\n    anyhow::bail!(\"refusing to operate on symlinked script: {}\", path.display());\n}\nlet config = manager.delete_user_script(&key)?;","typeGuard":null,"tryCatchPattern":"match manager.delete_user_script(&key) {\n    Ok(config) => { /* done */ }\n    Err(err) if err.to_string().contains(\"outside user script directory\") => {\n        // security signal: possible symlink/tampering — report, do NOT retry or delete manually from app code\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Never place symlinks inside the user scripts directory; copy scripts instead","Run script creation and deletion under the same trust boundary to narrow TOCTOU windows","Treat this specific error as a tampering indicator, not a transient failure"],"tags":["user-scripts","symlink","path-traversal","security","delete"],"backgroundTag":"symlink-path-traversal","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}