T09 · Insecure Skill Coding Practices
- Location
scripts/translate.py:19- Finding
Path Traversal Allows Reading JSON Files Outside the Scenario Directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/translate.py, lines 19-25
Vulnerability Type: Path traversal and unauthorized local file read
Risk Level: MediumVulnerable Code
python def load_scenario(self, scenario_name: str) -> Dict: """Load a scenario configuration.""" scenario_file = Path(self.scenario_path) / f"{scenario_name}.json" if not scenario_file.exists(): # Try to auto-detect scenario from user input return self._create_dynamic_scenario(scenario_name) with open(scenario_file, 'r', encoding='utf-8') as f: return json.load(f)Technical Analysis
The
scenario_namevalue is incorporated directly into a filesystem path without validation, normalization, an allowlist, or a containment check. Python'sPathjoining behavior does not prevent traversal components such as../from escaping the intendedscenariosdirectory.For example, a value such as
../../private/configcauses the application to construct a path equivalent to:text <project>/scenarios/../../private/config.jsonIf that path exists, the method opens it and returns its parsed contents. The target must be readable by the application process and contain valid JSON. The automatically appended
.jsonsuffix limits the issue to filenames that can be addressed with that suffix, but it does not enforce directory containment.The command-line interface does not directly print the complete returned dictionary. However,
load_scenario()is a public method, and callers integrating this class into an API, agent, or other service could expose the returned data. A traversed JSON document containing expected scenario fields could also be consumed by subsequent application logic.Attack Path
- An attacker gains control of the
scenario_nameargument, such as through the CLI or a service that wrapsIntentTranslator.load_scenario(). - The attack ...[truncated 1174 chars]
- An attacker gains control of the
- Remediation
View remediation
Remediation Suggestions
Use an explicit allowlist when scenario names are expected to refer only to bundled configurations:
python ALLOWED_SCENARIOS = { "career", "communication", "creative", "equity", "learning", } def load_scenario(self, scenario_name: str) -> Dict: if scenario_name not in ALLOWED_SCENARIOS: return self._create_dynamic_scenario(scenario_name) scenario_file = Path(self.scenario_path) / f"{scenario_name}.json" with scenario_file.open("r", encoding="utf-8") as f: return json.load(f)If arbitrary scenario filenames are a required feature, resolve the base and candidate paths and enforce containment before opening the file:
python def load_scenario(self, scenario_name: str) -> Dict: base = Path(self.scenario_path).resolve() if ( not scenario_name or scenario_name in {".", ".."} or "/" in scenario_name or "\\" in scenario_name ): raise ValueError("Invalid scenario name") candidate = (base / f"{scenario_name}.json").resolve() try: candidate.relative_to(base) except ValueError as exc: raise ValueError("Scenario path escapes the scenario directory") from exc if not candidate.is_file(): return self._create_dynamic_scenario(scenario_name) with candidate.open("r", encoding="utf-8") as f: scenario = json.load(f) return scenarioAdditional hardening measures should include:
- Validate loaded JSON against a strict schema before using it.
- Reject unexpected keys, invalid question identifiers, and malformed prompt templates.
- Run the Skill with least-privilege filesystem permissions.
- Avoid returning raw parser or filesystem errors to untrusted users.
- Add regression tests covering
../, absolute paths, mixed separators, symbolic links, missing files, and malformed JSON.
