T09 · Insecure Skill Coding Practices
Error
- Location
- src/calorie_calculator.py:458
- Finding
- Arbitrary Local File Contents Can Be Transmitted to External Vision APIs<![CDATA[ ## Vulnerability Details **File Location**: `src/calorie_calculator.py`, lines 56-58, 64-65, 94, 103-108, 137-138, 167, 176-181, and 458-469 **Vulnerability Type**: Insufficient file-type and path validation before external transmission **Risk Level**: High ### Vulnerable Code ```python def _encode_image(self, image_path: str) -> str: """Encode the image as Base64.""" with open(image_path, 'rb') as f: return base64.b64encode(f.read()).decode('utf-8') ``` The Kimi request reads the entire supplied file and represents it as a JPEG without validating its contents: ```python with open(image_path, 'rb') as f: base64_image = base64.b64encode(f.read()).decode('utf-8') payload = { "model": self.model, "messages": [ { "role": "user", "content": [ { "type": "text", "text": """Please identify the food in this image and return JSON.""" }, { "type": "image_url", "image_url": { "url": f"data:image/jpeg;base64,{base64_image}" } } ] } ], "max_tokens": 500 } response = requests.post( "https://api.moonshot.cn/v1/chat/completions", headers=headers, json=payload, timeout=30 ) ``` The OpenAI path repeats the same behavior: ```python with open(image_path, 'rb') as f: base64_image = base64.b64encode(f.read()).decode('utf-8') # ... "image_url": { "url": f"data:image/jpeg;base64,{base64_image}" } # ... response = requests.post( "https://api.openai.com/v1/chat/completions", headers=headers, json=payload, timeout=30 ) ``` The command-line entry point checks only whether the path exists: ```python image_path = sys.argv[1] if not os.path.exists(image_path): print(f"Error: image file does not exist: {image_path}") sys.exit(1) calculator = FoodCalo ...[truncated 2650 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Require a regular file and reject directories, devices, FIFOs, and special files: ```python path = Path(image_path).resolve(strict=True) if not path.is_file(): raise ValueError("Input must be a regular image file") ``` 2. Restrict access to a dedicated upload directory or to a trusted file handle supplied by the hosting platform. Resolve symbolic links before enforcing the boundary: ```python upload_root = Path("/srv/app/uploads").resolve() path = Path(image_path).resolve(strict=True) if upload_root not in path.parents: raise ValueError("Image is outside the approved upload directory") ``` 3. Set a conservative maximum encoded input size before reading the complete file. 4. Decode the file with Pillow and verify that it is a supported image: ```python with Image.open(path) as image: image.verify() with Image.open(path) as image: if image.format not in {"JPEG", "PNG", "WEBP"}: raise ValueError("Unsupported image format") ``` 5. Re-encode the decoded image into a canonical format before sending it. This ensures that arbitrary trailing or embedded file data is not forwarded. 6. Derive the media type from the verified image format rather than always labeling the payload as JPEG. 7. Display an explicit notice that the image will be transferred to the selected cloud provider and obtain user confirmation where appropriate. 8. Add tests proving that text files, symbolic links escaping the upload directory, oversized inputs, malformed images, and special files are rejected before any network request occurs. ]]>
