T09 · Insecure Skill Coding Practices
- Location
- scripts/freelance.py:238
- Finding
- Path Traversal in Job File Lookup<![CDATA[ ## Vulnerability Details **File Location**: `scripts/freelance.py`, lines 238-246 **Vulnerability Type**: Path traversal / arbitrary JSON file read **Risk Level**: Medium ### Vulnerable Code ```python # Load job job_file = self.jobs_dir / f"{job_id}.json" if not job_file.exists(): print(f"✗ Job not found: {job_id}") return None job_data = json.loads(job_file.read_text()) job = Job(**job_data) ``` The affected `job_id` value originates from the command-line interface: ```python prop_parser.add_argument('--job-id', required=True) ``` ### Technical Analysis The application constructs a file path by directly interpolating the user-controlled `job_id` into a filename. It does not reject absolute paths, path separators, or `..` traversal components. It also does not resolve the resulting path and verify that it remains within `self.jobs_dir`. Consequently, a value such as `../../external-job` can resolve to `../../external-job.json` outside the intended `~/.freelance-automator/jobs` directory. An absolute path can similarly override the base directory under `pathlib` path-joining semantics. The selected file must contain valid JSON whose keys are compatible with the `Job` dataclass. This requirement limits generic arbitrary-file disclosure, but an attacker can still read a compatible file outside the jobs directory. Values loaded from that file are subsequently inserted into the Ollama proposal prompt. If Ollama is unavailable, portions of the job data may also be reflected in the fallback proposal and displayed or stored. Because external job fields are treated as LLM prompt content without trust boundaries, a malicious compatible JSON document can additionally contain prompt-injection instructions that influence the generated proposal. This is a secondary consequence of the path traversal rather than a separate confirmed Agent instruction-hijacking issue. ### Attack Path 1. The attacker creates or identifies a readable JSON file outsi ...[truncated 1672 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Restrict job identifiers to the exact generated identifier format: ```python import re if not re.fullmatch(r"job-[0-9]+-[a-f0-9]{8}", job_id): raise ValueError("Invalid job ID") ``` 2. Resolve both paths and enforce containment within the jobs directory: ```python jobs_root = self.jobs_dir.resolve() job_file = (jobs_root / f"{job_id}.json").resolve() if not job_file.is_relative_to(jobs_root): raise ValueError("Job path escapes the jobs directory") ``` 3. Explicitly reject path separators, absolute paths, `.` components, and `..` components before accessing the filesystem. 4. Open files defensively and handle parsing and schema failures without exposing sensitive path or content information: ```python try: job_data = json.loads(job_file.read_text(encoding="utf-8")) job = Job(**job_data) except (OSError, json.JSONDecodeError, TypeError) as exc: raise ValueError("Invalid job record") from exc ``` 5. Validate loaded fields by type, length, and allowed character set before incorporating them into an LLM prompt. 6. Clearly delimit untrusted job data in the prompt and instruct the model to treat it only as data, not as instructions. Where proposal integrity is important, add output validation before printing or persisting generated content. 7. Add regression tests covering absolute paths, nested traversal sequences, encoded or mixed separators, nonexistent files, malformed JSON, and symbolic-link containment. ]]>
