T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/semi_auto_watcher.py:91
- Finding
- Symlink Following in Shared Media Directory Enables Arbitrary File Disclosure## Vulnerability Details **File Location**: `scripts/semi_auto_watcher.py:91-104`; `scripts/telegram_media_watcher.py:61-67,93-108` **Vulnerability Type**: Untrusted file processing and symbolic-link traversal **Risk Level**: High ### Vulnerable Code `scripts/semi_auto_watcher.py:91-104`: ```python def process(path: Path, state: dict): if path.suffix.lower() not in SUPPORTED: return if not stable(path): return digest = file_hash(path) if digest in state.get('processed', {}): return ts = int(time.time()) transcription = transcribe(path) stem = f'{path.stem}-{ts}' archived = DONE / f'{stem}{path.suffix.lower()}' shutil.copy2(path, archived) ``` `scripts/telegram_media_watcher.py:61-67`: ```python def process(path: Path): result = subprocess.run( ['python3', str(RUNNER), 'process', str(path), '--chat-id', CHAT_ID], capture_output=True, text=True, env=os.environ.copy(), ) return result.returncode, result.stdout, result.stderr ``` `scripts/telegram_media_watcher.py:93-108`: ```python for path in sorted(MEDIA_DIR.glob('*')): if not path.is_file(): continue if path.suffix.lower() not in SUPPORTED: continue if not stable(path): continue digest = file_hash(path) if digest in state.get('processed', {}): continue code, out, err = process(path) meta = {'file': str(path), 'hash': digest, 'time': int(time.time()), 'code': code, 'stdout': out, 'stderr': err} if code == 0: parsed = parse_sections(out) meta.update(parsed) ``` ### Technical Analysis Both watchers process files found under the shared temporary directory `/tmp/picoclaw_media`. The validation checks only whether an entry appears to be a file, has a supported filename suffix, and has a stable size. `Path.is_file()`, `Path.s ...[truncated 2424 chars]
- Remediation
- ## Remediation Suggestions 1. Reject symbolic links before processing: ```python st = path.lstat() if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode): return ``` 2. Open files through a file descriptor using `os.open()` with `O_NOFOLLOW`, where supported, and perform `fstat()` on that descriptor. 3. Resolve the candidate path and verify that it remains strictly beneath the trusted media directory: ```python media_root = MEDIA_DIR.resolve(strict=True) candidate = path.resolve(strict=True) if candidate.parent != media_root: return ``` This containment check should supplement, not replace, `O_NOFOLLOW`. 4. Verify that the file is owned by the expected Picoclaw runtime account and is not writable by untrusted users. 5. Use a private directory with restrictive permissions instead of a generally accessible `/tmp` location. 6. Accept only files referenced by authenticated Telegram message metadata rather than processing every matching directory entry. 7. Enforce strict file-size, duration, and processing-time limits before hashing, copying, or uploading content. 8. Copy trusted input into a private directory through a safely opened descriptor and process that immutable copy. 9. Run the watcher as a dedicated, unprivileged service account with access only to the required media and state directories.
