T09 · Insecure Skill Coding Practices
Error
- Location
- canary.py:97
- Finding
- Protected Path Controls Can Be Bypassed Through Symbolic Links<![CDATA[ ## Vulnerability Details **File Location**: `canary.py`, lines 97-110 **Vulnerability Type**: Improper path canonicalization and unsafe prefix comparison **Risk Level**: High ### Vulnerable Code ```python # Expand home directory expanded_path = os.path.expanduser(path) abs_path = os.path.abspath(expanded_path) # Check against protected paths for protected in self.protected_paths: protected_expanded = os.path.expanduser(protected) protected_abs = os.path.abspath(protected_expanded) if abs_path.startswith(protected_abs): reason = f"Canary: Protected path access blocked: {path}" self._log_violation( 'critical', f"Attempted {operation} on protected path: {path}" ) return False, reason ``` ### Technical Analysis `os.path.abspath()` normalizes relative components such as `..`, but it does not resolve symbolic links. Consequently, the lexical path checked by Canary can differ from the filesystem object ultimately accessed. For example, if `/tmp/safe-link` is a symbolic link to `/etc`, checking `/tmp/safe-link/passwd` does not produce a path beginning with `/etc/`, even though opening that path accesses `/etc/passwd`. The use of `str.startswith()` is also not component-aware. If a protected path is configured as `/etc` without a trailing separator, an unrelated path such as `/etc-backup/file` is incorrectly classified as protected. This produces false positives in addition to the symlink-based false negatives. The project documentation acknowledges that symlink attacks may bypass path checks, but the weakness remains in the security enforcement implementation. ### Attack Path 1. An attacker or monitored agent creates a symbolic link in an unprotected directory: ```bash ln -s /etc /tmp/safe-link ``` 2. The agent asks Canary to validate the lexical path: ```python allowed, reason = canary.check_path( "/tmp/safe-link/passwd", "read" ) ``` 3. `os.path ...[truncated 961 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Canonicalize both candidate and protected paths using `Path.resolve()`: ```python candidate = Path(path).expanduser().resolve(strict=False) for protected in self.protected_paths: protected_path = Path(protected).expanduser().resolve(strict=False) if candidate == protected_path or protected_path in candidate.parents: return False, "Protected path access blocked" ``` 2. Use component-aware comparisons rather than raw string prefixes. 3. Revalidate the path immediately before the filesystem operation to reduce time-of-check/time-of-use exposure. 4. Where possible, open files relative to trusted directory descriptors and use platform controls that reject symbolic links, such as `O_NOFOLLOW`. 5. Add regression tests for: - Symlinks into protected directories. - Nested symlinks. - `..` traversal. - Paths that merely share a textual prefix. - Nonexistent write targets whose parent contains symlinks. 6. Continue to enforce OS-level permissions and sandboxing because application-level path checks cannot replace filesystem access controls. ]]>
