T09 · Insecure Skill Coding Practices
Error
- Location
- vinrecognition.py:30
- Finding
- Symbolic-Link Bypass Allows Unintended Local File Disclosure to an External API<![CDATA[ ## Vulnerability Details **File Location**: `vinrecognition.py`, lines 30-48, 95-108, and 139-140 **Vulnerability Type**: Insufficient path containment validation and external disclosure of local file contents **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} ``` ```python path = safe["path"] if not os.path.isfile(path): return {"pic": None, "error": f"File not found: {safe['relative']}"} try: with open(path, "rb") as f: raw = f.read() except Exception as e: return {"pic": None, "error": f"Failed to read file: {e}"} try: encoded = base64.b64encode(raw).decode("utf-8") except Exception as e: return {"pic": None, "error": f"Failed to base64-encode file: {e}"} ``` ```python result = _call_vin_api(appkey, pic_info["pic"]) print(json.dumps(result, ensure_ascii=False, indent=2)) ``` The encoded content is transmitted by `_call_vin_api` as follows: ```python VIN_RECOG_URL = "https://api.jisuapi.com/vinrecognition/recognize" def _call_vin_api(appkey: str, pic_base64: str) -> Dict[str, Any]: params = {"appkey": appkey} data = {"pic": pic_base64} try: resp = requests.post(VIN_RECOG_URL, params=params, da ...[truncated 2699 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve the canonical working directory and requested path before opening the file: ```python from pathlib import Path base = Path.cwd().resolve() candidate = (base / user_path).resolve(strict=True) try: candidate.relative_to(base) except ValueError: return { "error": "invalid_path", "message": f"Path escapes the allowed directory for '{field}'", } ``` 2. Explicitly reject symbolic links when they are unnecessary for the Skill: ```python unresolved = base / user_path if unresolved.is_symlink(): return { "error": "invalid_path", "message": "Symbolic links are not allowed", } ``` If nested path components must also be protected, inspect every component or use a platform-supported safe-open mechanism. 3. Reduce time-of-check-to-time-of-use risk. On supported systems, open files using `os.open()` with `O_NOFOLLOW`, then validate the opened file descriptor with `os.fstat()` before reading it. 4. Confirm that the opened object is a regular file and enforce a strict maximum size before loading it into memory. The documented API indicates a 300 KB image limit, so rejecting larger files locally would reduce both disclosure scope and resource exhaustion risk. 5. Validate the file as an allowed image format using content-based checks rather than trusting its name or extension. Reject malformed and unsupported content before any network transmission. 6. Use a dedicated upload directory with restrictive permissions instead of allowing access to the entire current working directory. 7. Inform users clearly that selected images are transmitted to JisuAPI. Where supported by the provider, pass the API key through an authorization header rather than a URL query parameter to reduce exposure in proxy and server URL logs. ]]>
