T09 · Insecure Skill Coding Practices
Error
- Location
- qrcode.py:10
- Finding
- Symlink-Based Escape from the Intended Filesystem Boundary<![CDATA[ ## Vulnerability Details **File Location**: `qrcode.py:10-40`; affected callers at `qrcode.py:91-99` and `qrcode.py:120-130` **Vulnerability Type**: Insufficient path validation and symlink traversal **Risk Level**: High ### Vulnerable Code ```python def _normalize_local_path(user_path: str, field: str) -> Dict[str, Any]: """ 规范化并限制本地文件路径,只允许在当前工作目录及其子目录内读写。 禁止绝对路径和目录穿越(包含 ..)。 """ if not user_path: return { "error": "invalid_param", "message": f"field '{field}' is empty", } # 禁止绝对路径 if os.path.isabs(user_path): return { "error": "invalid_path", "message": f"Absolute path is not allowed for '{field}'", } # 规范化并检查目录穿越 norm = os.path.normpath(user_path) if norm.startswith("..") or norm == "..": return { "error": "invalid_path", "message": f"Path traversal is not allowed for '{field}'", } base = os.getcwd() full = os.path.join(base, norm) return {"error": None, "path": full, "relative": norm} ``` The resulting path is used for output as follows: ```python safe = _normalize_local_path(out_raw, "out") if safe["error"]: return safe out = safe["path"] out_dir = os.path.dirname(out) if out_dir: os.makedirs(out_dir, exist_ok=True) try: img.save(out) except Exception as e: return {"error": "save_failed", "message": str(e), "path": out} ``` It is also used for input as follows: ```python safe = _normalize_local_path(path_raw, "path") if safe["error"]: return safe path = safe["path"] if not os.path.isfile(path): return {"error": "file_not_found", "message": f"File not found: {safe['relative']}"} img = cv2.imread(path) ``` ### Technical Analysis The path validation is lexical. It rejects absolute paths and normalized strings beginning with `..`, but it does not resolve symbolic links before deciding whether a path remains inside the current working directory. ...[truncated 2133 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve both the trusted base directory and candidate path: ```python base = os.path.realpath(os.getcwd()) candidate = os.path.realpath(os.path.join(base, norm)) if os.path.commonpath([base, candidate]) != base: return { "error": "invalid_path", "message": f"Path escapes the working directory for '{field}'", } ``` 2. Reject symbolic links in every existing component of the candidate path when symlinks are not required by the feature. 3. For output files that do not yet exist, validate the resolved parent directory separately. 4. Reduce time-of-check/time-of-use exposure by using descriptor-relative filesystem operations and no-follow semantics, such as `openat`-style APIs and `O_NOFOLLOW`, where supported. 5. For output, reject an existing destination if it is a symbolic link and consider exclusive creation when overwriting is not required. 6. Add tests covering: - A direct absolute path. - `../` traversal. - A symlinked input file. - A symlinked parent directory. - A symlinked output destination. - A symlink changed between validation and access. 7. Remove the duplicated implementation so that the corrected validation logic has only one authoritative definition. ]]>
