T09 · Insecure Skill Coding Practices
- Location
scripts/camera_helper.py:18- Finding
Unrestricted URL Retrieval and Arbitrary File Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
scripts/camera_helper.py, lines 18-37 and 72-87
Vulnerability Type: Server-Side Request Forgery and Arbitrary File Write
Risk Level: MediumVulnerable Code
python def download_image(url: str, output_path: str): """Download an image from the Node device HTTP server.""" try: urllib.request.urlretrieve(url, output_path) size_kb = Path(output_path).stat().st_size / 1024 print(f"Image downloaded: {output_path} ({size_kb:.1f} KB)") return True except Exception as e: print(f"Download failed: {e}") return False def decode_base64_frame(frame_data: str, output_path: str): """Decode a Base64 image frame and save it.""" try: if "," in frame_data: frame_data = frame_data.split(",")[1] image_bytes = base64.b64decode(frame_data) Path(output_path).write_bytes(image_bytes) print(f"Image saved: {output_path}") return True except Exception as e: print(f"Decode failed: {e}") return FalseThe command-line arguments and dispatch logic that expose these operations are:
python parser.add_argument('--download', help='Download an image from the Node HTTP server (URL)') parser.add_argument('--decode-frame', help='Base64 frame data file path') parser.add_argument('--output', '-o', default='output.jpg', help='Output image path') parser.add_argument('--analyze', help='Analyze a frame JSON file') args = parser.parse_args() if args.download: success = download_image(args.download, args.output) sys.exit(0 if success else 1) elif args.decode_frame: frame_data = Path(args.decode_frame).read_text().strip() success = decode_base64_frame(frame_data, args.output) sys.exit(0 if success else 1)The snippets above are translated into English for explanatory consistency; the execu ...[truncated 3526 chars]
- Remediation
View remediation
Remediation Suggestions
-
Apply a strict URL allowlist
- Permit only the schemes required by the Skill, preferably plain
httponly if the Node protocol cannot support TLS. - Require the configured paired Node hostname or IP address.
- Require port
18790. - Require the normalized URL path to begin with
/images/. - Reject embedded credentials, fragments, unexpected query parameters, and malformed authority components.
- Permit only the schemes required by the Skill, preferably plain
-
Validate resolved addresses
- Resolve the hostname before connecting.
- Reject loopback, link-local, multicast, unspecified, and unrelated private or internal addresses unless the configured paired Node explicitly uses an approved private address.
- Revalidate redirects and either disable them or enforce the same allowlist on every redirect target.
- Consider DNS rebinding by ensuring that the validated address is the address used for the connection.
-
Restrict output destinations
- Create a dedicated output directory with restrictive permissions.
- Resolve the requested destination with
Path.resolve(). - Verify that the resolved destination remains beneath the approved output directory.
- Reject absolute paths, parent-directory traversal, symlink destinations, and special files.
- Refuse to overwrite existing files by default, using exclusive creation or a safely generated filename.
-
Limit resource consumption
- Replace
urlretrievewith controlled streaming that applies explicit connection and read timeouts. - Enforce a maximum response size before and during download.
- Enforce maximum input and decoded sizes for Base64 frames.
- Decode with strict validation, such as
base64.b64decode(frame_data, validate=True).
- Replace
-
Validate downloaded content
- Check the HTTP status and expected content type.
- Verify the JPEG signature and parse the image with a safe image decoder before accepting it.
- Delet ...[truncated 447 chars]
-
