T09 · Insecure Skill Coding Practices
- Location
state_machine.py:248- Finding
Unrestricted Checkpoint Paths Allow Arbitrary JSON File Overwrite and Unsafe State Loading
- Content
View full analysis
Vulnerability Details
File Location:
state_machine.py:248-279
Vulnerability Type: Path traversal, unsafe file handling, and plaintext storage of potentially sensitive workflow state
Risk Level: MediumVulnerable Code
python def save_checkpoint(self, name: Optional[str] = None) -> str: """Save the current checkpoint to disk.""" self.CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True) filename = name or f"checkpoint_{int(time.time())}.json" filepath = self.CHECKPOINT_DIR / filename checkpoint_data = self.checkpoint.model_dump(mode="json") with open(filepath, "w", encoding="utf-8") as f: json.dump(checkpoint_data, f, ensure_ascii=False, indent=2, default=str) # Retain only the latest MAX_CHECKPOINTS checkpoint files. all_checkpoints = sorted( self.CHECKPOINT_DIR.glob("checkpoint_*.json"), key=lambda p: p.stat().st_mtime, reverse=True, ) for old_file in all_checkpoints[self.MAX_CHECKPOINTS:]: old_file.unlink() return str(filepath) def load_checkpoint(self, name: str) -> bool: """Restore a checkpoint from disk.""" filepath = self.CHECKPOINT_DIR / name if not filepath.exists(): return False with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) self.checkpoint = WorkflowCheckpoint(**data) self.current_phase = self.checkpoint.phase return TrueThe checkpoint contains potentially sensitive workflow information:
python class WorkflowCheckpoint(BaseModel): """Workflow checkpoint used for recovery.""" phase: WorkflowPhase tier: Tier input_data: Optional[UserInput] = None strategy_rounds: List[StrategyRound] = Field(default_factory=list) task_dag: Optional[TaskDAG] = None execution_report: Optional[ExecutionReport] = None review_results: List[DecisionResult] = Field(default_fact ...[truncated 4105 chars]- Remediation
View remediation
Remediation Suggestions
-
Restrict checkpoint names
- Accept only a basename matching a strict allowlist, such as
checkpoint_[A-Za-z0-9_-]+\.json. - Reject absolute paths, path separators,
.components, and..components. - Prefer generating checkpoint identifiers internally rather than accepting filenames from callers.
- Accept only a basename matching a strict allowlist, such as
-
Enforce canonical path containment
- Resolve the checkpoint directory and candidate path.
- Verify that the resolved candidate is a descendant of the resolved checkpoint directory before reading or writing.
- Reject symlinks or open files using operating-system features that prevent symlink following where available.
-
Harden filesystem permissions
- Create
.triangulate_checkpointswith permissions equivalent to0700. - Create checkpoint files with permissions equivalent to
0600. - Do not rely exclusively on the process umask.
- Create
-
Use safe write semantics
- Write to a securely created temporary file inside the checkpoint directory.
- Flush and atomically replace the destination after serialization succeeds.
- Use exclusive creation when overwriting is not explicitly required.
-
Protect stored data
- Omit or redact user input, report content, and session identifiers unless they are required for recovery.
- Encrypt checkpoints when sensitive workflow content must be persisted.
- Define an explicit retention and secure-deletion policy.
-
Validate loaded checkpoints
- Enforce a maximum file size before parsing.
- Catch JSON decoding, I/O, and Pydantic validation failures.
- Validate that loaded workflow phases and state transitions are appropriate for recovery.
- Consider signing checkpoints to prevent unauthorized state modification.
Example containment logic:
python import re from pathlib import Path CHECKPOINT_NAME = re.compile(r"^checkpoint_[A-Za-z0-9_-]+\.json$") def _safe_ch ...[truncated 370 chars]-
