T09 · Insecure Skill Coding Practices
Error
- Location
- tapo-capture.py:20
- Finding
- Unrestricted Camera Host May Receive Sensitive Authentication Material## Vulnerability Details **File Location**: `tapo-capture.py:20-128` **Vulnerability Type**: Unvalidated destination for credential-bearing authentication **Risk Level**: High ### Vulnerable Code ```python parser.add_argument("--host", required=True, help="Camera hostname or IP") ``` ```python username = os.getenv("TAPO_CAMERA_USERNAME") password = os.getenv("TAPO_CAMERA_PASSWORD") credentials_hash = os.getenv("KASA_CREDENTIALS_HASH") if credentials_hash and (username or password): raise SystemExit( "Use either TAPO_CAMERA_USERNAME/TAPO_CAMERA_PASSWORD or KASA_CREDENTIALS_HASH, not both." ) if username or password: if not username or not password: raise SystemExit( "Both TAPO_CAMERA_USERNAME and TAPO_CAMERA_PASSWORD are required together." ) credentials = Credentials(username=username, password=password) else: credentials = None ``` ```python return DeviceConfig( host=host, timeout=timeout, credentials=credentials, credentials_hash=credentials_hash, connection_type=camera_connection, ) ``` ```python config = build_config(args.host, args.timeout) dev = await Device.connect(config=config) ``` ### Technical Analysis The mandatory `--host` argument is accepted without validating that it identifies a user-approved camera on the trusted local network. The value is inserted directly into a `DeviceConfig` alongside either the camera username and password or `KASA_CREDENTIALS_HASH`, after which `python-kasa` initiates authentication with the selected destination. This implementation does not enforce the LAN-only boundary documented elsewhere in the project. It does not: - Resolve and classify the supplied address before authentication. - Reject public, unspecified, multicast, loopback, or otherwise unauthorized destinations. - Compare the destination against an allowlist of approved camera addresses. ...[truncated 2304 chars]
- Remediation
- ## Remediation Suggestions 1. **Resolve and validate the destination before loading or using credentials.** - Resolve the supplied hostname with `socket.getaddrinfo`. - Parse every result with Python's `ipaddress` module. - Require the address to belong to an explicitly approved LAN subnet rather than relying only on `is_private`. - Reject public, unspecified, multicast, reserved, and loopback addresses. Handle link-local addresses only through a documented, explicit exception. 2. **Use an explicit camera allowlist.** - Store user-approved camera IP addresses or stable device identifiers without credentials. - Require confirmation before adding a new destination. - Refuse credential-bearing connections to hosts absent from the allowlist. 3. **Mitigate DNS rebinding and time-of-check/time-of-use issues.** - Resolve the hostname once. - Validate the resolved address. - Connect to the validated address or otherwise pin resolution for the connection. - If hostname-based TLS verification is required, bind the validated IP and expected hostname securely rather than performing an independent second lookup. 4. **Validate before reading secrets where practical.** - Complete destination authorization before retrieving credentials from environment variables or a secret manager. - Keep secrets scoped to the shortest possible execution period. 5. **Fail closed on ambiguous resolution.** - Reject hostnames that resolve to a mixture of approved and unapproved addresses. - Report the resolved address without printing credentials or authenticated URLs. 6. **Add security regression tests.** - Confirm that approved RFC1918 camera addresses are accepted only when they are in configured LAN ranges. - Confirm that public IP addresses, loopback, multicast, unspecified addresses, mixed DNS results, and rebinding-style resolution changes are rejected before `Device.connect()` is called.
