T09 · Insecure Skill Coding Practices
Error
- Location
- crabpath/split.py:397
- Finding
- Workspace Symlink Boundary Bypass Can Expose Files to OpenAI<![CDATA[ ## Vulnerability Details **File Location**: `crabpath/split.py:397-411`; network transmission occurs through `examples/openclaw_adapter/init_agent_brain.py:289-317` **Vulnerability Type**: Workspace boundary bypass and sensitive-data disclosure through symlink traversal **Risk Level**: High ### Vulnerable Code ```python for filename in sorted(file_names): rel = (rel_dir / filename).as_posix() if rel_dir.parts else filename file_path = Path(dir_path) / filename if not file_path.is_file() or file_path.suffix.lower() not in extensions: continue if _should_skip_path(rel, excludes, gitignore_patterns): continue candidates.append((file_path, rel)) split_plan: list[tuple[int, str, str, bool]] = [] for file_path, rel in candidates: text = file_path.read_text(encoding="utf-8") ``` The resulting text is passed to the OpenAI embedding callback: ```python graph, texts = split_workspace(str(workspace), llm_fn=None, llm_batch_fn=None) # ... embeddings = batch_or_single_embed( list(texts.items()), embed_batch_fn=embed_batch, ) ``` The callback submits the raw content: ```python response = client.embeddings.create( model=OPENAI_EMBEDDING_MODEL, input=list(contents), ) ``` ### Technical Analysis `Path.is_file()` and `Path.read_text()` follow symbolic links. The workspace scanner checks the lexical relative filename against exclusions, but it does not resolve each candidate to its canonical path and confirm that the resolved path remains beneath the canonical workspace directory. Consequently, a supported file inside the workspace can be a symbolic link to an arbitrary readable file outside the workspace. The scanner tests the extension of the symlink name rather than the resolved target, so an attacker can use a name such as `reference.json` even when the external target has another name. The default supported extensions include `.json`, `.yaml`, `.yml`, `.toml`, `.cfg`, `.ini`, `.md`, and `.txt`. Th ...[truncated 2388 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Reject symbolic links by default: ```python if file_path.is_symlink(): continue ``` 2. Enforce canonical workspace containment before reading: ```python workspace_root = workspace.resolve() try: resolved_file = file_path.resolve(strict=True) resolved_file.relative_to(workspace_root) except (FileNotFoundError, RuntimeError, ValueError): continue if not resolved_file.is_file(): continue ``` 3. Open files defensively to reduce check-to-use races. On supported platforms, use a directory file descriptor and `O_NOFOLLOW`, then verify with `fstat()` that the opened object is a regular file. 4. Apply the same containment policy to directory entries and explicitly prohibit traversal through symlinked directories, even if future code enables `os.walk(..., followlinks=True)`. 5. Add default exclusions for common sensitive filenames and patterns, including credential files, private keys, token stores, and provider configuration. Secret detection should supplement—not replace—the canonical containment check. 6. Add a preview or confirmation mode that lists every file whose contents will be sent to a network-backed embedder. 7. Update the OpenAI adapter documentation to state explicitly that raw workspace chunks and optional learning-record text are transmitted to the configured provider. 8. Add regression tests covering: - A file symlink to a target outside the workspace - A symlink with a supported name pointing to a differently named target - A symlink to a sensitive file - A symlink remaining within the workspace, according to the intended policy - A directory symlink escaping the workspace ]]>
