T09 · Insecure Skill Coding Practices
Warning
- Location
- src/pypi_package_changelog_generator/archive_diff.py:88
- Finding
- Unbounded Source Archive Processing Enables Resource Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `src/pypi_package_changelog_generator/_http.py:83-90`, `src/pypi_package_changelog_generator/pypi_client.py:39-54`, `src/pypi_package_changelog_generator/archive_diff.py:88-105`, and `src/pypi_package_changelog_generator/archive_diff.py:156-166` **Vulnerability Type**: Unbounded download, archive extraction, and in-memory file processing **Risk Level**: Medium ### Vulnerable Code `src/pypi_package_changelog_generator/_http.py:83-90`: ```python with self._opener.open(urllib_request, timeout=self._timeout) as response: return HttpResponse( status_code=response.status, headers=_normalize_headers(response.headers.items()), content=response.read(), url=response.geturl(), ) ``` `src/pypi_package_changelog_generator/pypi_client.py:39-54`: ```python def download_bytes(self, url: str) -> bytes: try: response = self._client.get(url) except HttpTransportError as exc: raise PypiClientError( code="pypi_download_failed", message=f"Failed to download source archive from {url}.", retryable=True, ) from exc if response.status_code >= 400: raise PypiClientError( code="pypi_download_failed", message=f"Failed to download source archive from {url}.", retryable=True, ) return response.content ``` `src/pypi_package_changelog_generator/archive_diff.py:88-105`: ```python def extract_archive(content: bytes) -> ExtractedArchive: temp_dir = tempfile.TemporaryDirectory(prefix="pypi-changelog-") root = Path(temp_dir.name) root_resolved = root.resolve() try: with tarfile.open(fileobj=BytesIO(content), mode="r:gz") as archive: members = archive.getmembers() for member in members: if not _is_safe_tar_member(root_resolved, member): raise ArchiveDiffError( code="uns ...[truncated 3639 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Stream downloads with a strict maximum size** - Read responses in bounded chunks instead of using unrestricted `response.read()`. - Abort once a configurable maximum compressed size is exceeded. - Validate `Content-Length` when present, while still enforcing the streaming limit because the header may be absent or inaccurate. 2. **Restrict archive sources** - Validate source distribution URLs against expected PyPI file-hosting domains and require HTTPS. - Revalidate the destination after every redirect. - Reject unexpected schemes, embedded credentials, and unapproved hosts. 3. **Enforce extraction limits before writing files** - Cap the number of archive members. - Cap each member's declared and actual extracted size. - Cap aggregate expanded size. - Reject archives with excessive compression ratios. - Continue rejecting traversal paths, links, devices, FIFOs, and other special entries. 4. **Bound recursive analysis** - Stop traversal after a maximum file count or aggregate byte count. - Skip files exceeding a per-file analysis limit. - Read large files incrementally for hashing instead of retaining all content. - Retain file content only for files selected for detailed diff generation. 5. **Apply budgets before expensive diff construction** - Identify and prioritize candidate files using metadata and bounded hashes first. - Generate patches only for the limited final file set. - Cap line count and input bytes passed to `difflib`. 6. **Add operational safeguards** - Configure execution timeouts, memory limits, and temporary-storage quotas. - Return a structured warning when an archive exceeds a limit. - Add tests covering oversized responses, excessive members, large files, and high-ratio compressed archives. ]]>
