# Security Completion candidates flow into shell interpreters as part of command lines, so conda-completion applies defense-in-depth against injection and data corruption. ## Trust boundaries Three boundaries matter: 1. Manifest generation (Python, runs as the user) introspects conda's argparse tree and writes the manifest. Input is trusted (conda's own parser), but the output path needs symlink protection. 2. File I/O (Python and Rust) reads and writes cache files, manifests, and version data. Files on disk could be tampered with by other processes. 3. Shell output (Rust, runs on TAB press) produces text consumed by the shell's completion system. Malicious data in the manifest or project files could attempt to inject shell commands. ## Output sanitization The Rust binary sanitizes all completion candidates before emitting them. `sanitize()` uses an allowlist of permitted characters: - Alphanumerics (`a-z`, `A-Z`, `0-9`) - Hyphens (`-`), dots (`.`), underscores (`_`) - Slashes (`/`), equals (`=`), at signs (`@`) - Spaces, colons (`:`), plus (`+`), tilde (`~`), backslash (`\`) Everything else is stripped, blocking shell metacharacters like `$`, backticks, semicolons, pipes, and parentheses from reaching the shell interpreter. The function uses `Cow` to skip allocation when the input is already clean (the common case). ### Per-shell escaping After sanitization, shell-specific escaping is applied: - Zsh: colons and backslashes are escaped (`\:`, `\\`) because zsh uses colons as delimiters in `_describe`. - Fish and PowerShell: tab-separated `candidate\tdescription` format. - Bash: one candidate per line, no descriptions (`compgen`/`complete` does not support them). ## Symlink protection Both Python and Rust refuse cache-file symlinks: Python (`atomic_write` in `manifest.py`) checks `path.is_symlink()` before and after creating the temp file. If the target is a symlink at either point, the write is aborted and the temp file is removed. The double-check narrows the TOCTOU window, though it does not eliminate it entirely on all filesystems. Python manifest and version readers also reject symlinked `completion.msgpack`, `versions.index`, and `versions.store` files. Rust (`StatCache.save` in `cache.rs`) checks `symlink_metadata(path)` before writing. Uses `tempfile::NamedTempFile` with `O_EXCL` semantics to create the temp file, then calls `persist()` to atomically rename it. Rust (`file_stat`, `read_to_string_limited`, `read_to_bytes_limited` in `cache.rs`) uses `symlink_metadata()` on all file reads to verify the path is a regular file. Symlinked files are silently skipped. ## Atomic file operations All file writes use a temp-file-then-rename pattern: 1. Create a temporary file in the same directory as the target. 2. Write all data to the temp file. 3. Rename the temp file to the target path. Readers never see a partially-written file. If the process crashes mid-write, only an orphaned temp file remains. On POSIX systems, `rename(2)` is atomic within a single filesystem. ## File size limits Both Python and Rust enforce size limits on file reads: - Manifest: 50 MB (`MAX_MANIFEST_SIZE` in Python) - Project files: 10 MB (`MAX_FILE_SIZE` in Rust) - Collection sizes: 2,000,000 entries for arrays and maps The Python side passes these to msgpack's deserialization functions (`max_str_len`, `max_bin_len`, `max_array_len`, `max_map_len`). The Rust side uses `rmp_serde` with post-load length checks. ## No shell execution The Rust binary never executes shell commands, subprocesses, or `eval`-like operations. It reads files, deserializes data, matches strings, and writes text to stdout. The shell's own completion machinery interprets the output. Startup hooks may use shell-native evaluation (`eval`, `source`, or `Invoke-Expression`) to load the generated completion function. During completion, that function executes the Rust binary directly and consumes its stdout; the binary itself is a pure data processor. ## Summary | Threat | Mitigation | | --- | --- | | Shell injection via completion output | Allowlist sanitization + per-shell escaping | | Symlink attack on cache/manifest/version files | Symlink rejection + atomic rename | | Partial file reads (crash mid-write) | Temp-file-then-rename for all writes | | Oversized input files | Size limits on all file reads (10-50 MB) | | Malformed msgpack data | Size-bounded deserialization + type validation | | Stale data after plugin changes | Automatic hash-based regeneration |