T09 · Insecure Skill Coding Practices
Warning
- Location
- pkg/skill_health/load.py:49
- Finding
- Unbounded CSV and Compressed ZIP Processing Can Cause Resource Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `pkg/skill_health/load.py:33-57`, with the ZIP loading path invoked at `pkg/skill_health/load.py:362-383` **Vulnerability Type**: Uncontrolled resource consumption through unbounded input processing **Risk Level**: Medium ### Vulnerable Code ```python def _load_csv(path: Path) -> pd.DataFrame | None: """Read a CSV file from disk. Returns None if missing or empty.""" if not path.exists(): return None try: raw_df = pd.read_csv(path) except Exception as e: logger.warning("Could not read %s: %s", path, e) return None if raw_df.empty: return None return raw_df def _load_csv_from_zip( zip_file: zipfile.ZipFile, member_name: str ) -> pd.DataFrame | None: """Read a CSV member from an open ZIP. Returns None if missing or empty.""" if member_name not in zip_file.namelist(): return None try: with zip_file.open(member_name) as f: raw_df = pd.read_csv(f) except Exception as e: logger.warning("Could not read %s from ZIP: %s", member_name, e) return None if raw_df.empty: return None return raw_df ``` The affected ZIP-loading path is: ```python if data_path.suffix.lower() == ".zip": with zipfile.ZipFile(data_path, "r") as zip_file: steps_df = _normalize_and_dedupe_steps( _load_csv_from_zip(zip_file, "steps.csv") ) heart_rate_df = _normalize_and_dedupe_heart_rate( _load_csv_from_zip(zip_file, "heart_rate.csv") ) calories_df = _normalize_and_dedupe_calories( _load_csv_from_zip(zip_file, "calories.csv") ) sleep_sessions_df = _normalize_sleep( _load_csv_from_zip(zip_file, "sleep_sessions.csv") ) exercise_sessions_df = _normalize_exercise( _load_csv_from_zip(zip_file, "exercise_sessions.csv") ) oxygen_saturation_df = _normalize_oxy ...[truncated 2862 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Enforce ZIP metadata limits before opening members** - Retrieve each expected member through `ZipFile.getinfo()`. - Reject members whose `file_size` exceeds a defined expanded-size limit. - Reject archives whose total expanded size exceeds an aggregate limit. - Reject suspicious compression ratios calculated from `file_size` and `compress_size`. - Handle zero-byte compressed sizes safely when calculating ratios. 2. **Restrict accepted archive contents** - Permit only the expected CSV member names. - Reject duplicate member names, encrypted members, and unsupported entry types. - Continue streaming members rather than extracting them to disk. 3. **Apply equivalent controls to ordinary CSV files** - Check `Path.stat().st_size` before calling `pandas.read_csv()`. - Reject files exceeding a documented maximum size. - Ensure that symbolic links and caller-selected input paths are handled according to the deployment trust model. 4. **Use bounded CSV parsing** - Parse large inputs with `pandas.read_csv(..., chunksize=...)`. - Track cumulative rows and bytes and abort when configured limits are reached. - Validate required columns early so irrelevant or malformed data is not processed unnecessarily. - Select only required columns and specify appropriate data types where practical to reduce memory use. 5. **Add runtime containment** - Run analysis with process-level memory, CPU, and execution-time limits. - Return a clear validation error when an input exceeds limits rather than attempting to process it. - Avoid exposing detailed host resource information in error messages. 6. **Add adversarial tests** - Test oversized plain CSV files. - Test highly compressed ZIP members. - Test archives containing multiple large expected members. - Verify that rejected inputs fail before substantial decompression or DataFrame allocation occurs. ]]>
