Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security

- Inspect hidden ZIP archives and malicious pickle payloads in legacy GGML model variants.
### Bug Fixes

- Avoid network false positives for bounded README examples that download sample images over HTTPS from Hugging Face.
Expand Down
11 changes: 6 additions & 5 deletions modelaudit/scanners/gguf_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,8 +760,8 @@ def _scan_gguf(self, f: BinaryIO, file_size: int, result: ScanResult) -> None:
)
result.bytes_scanned = max(result.bytes_scanned, f.tell())

def _scan_zip_polyglot(self, result: ScanResult) -> bool:
"""Inspect ZIP members even when GGUF metadata or tensor parsing fails."""
def _scan_zip_polyglot(self, result: ScanResult, *, format_name: str = "GGUF") -> bool:
"""Inspect ZIP members even when GGUF/GGML header parsing fails."""
if not zipfile.is_zipfile(self.current_file_path):
return False

Expand All @@ -776,7 +776,7 @@ def _scan_zip_polyglot(self, result: ScanResult) -> bool:
self.current_file_path,
result,
archive_config,
context="GGUF trailing ZIP polyglot",
context=f"{format_name} trailing ZIP polyglot",
Comment thread
mldangelo-oai marked this conversation as resolved.
)
rejected_paths = result._private_metadata.get(
_ZIP_CONTAINER_PREFLIGHT_REJECTED_PATHS_PRIVATE_METADATA_KEY,
Expand All @@ -789,9 +789,9 @@ def _scan_zip_polyglot(self, result: ScanResult) -> bool:
return False

result.add_check(
name="GGUF ZIP Polyglot Detection",
name=f"{format_name} ZIP Polyglot Detection",
passed=False,
message="GGUF file is also a valid ZIP archive and may contain hidden archive content",
message=f"{format_name} file is also a valid ZIP archive and may contain hidden archive content",
severity=IssueSeverity.CRITICAL,
location=self.current_file_path,
details={"embedded_format": "zip"},
Expand Down Expand Up @@ -1050,6 +1050,7 @@ def _scan_ggml(
"""Basic GGML file validation with security checks."""
result.metadata["format"] = "ggml"
result.metadata["magic"] = magic.decode("ascii", "ignore")
self._scan_zip_polyglot(result, format_name="GGML")
Comment thread
mldangelo-oai marked this conversation as resolved.
Comment thread
mldangelo-oai marked this conversation as resolved.

if file_size < 32:
result.add_check(
Expand Down
45 changes: 45 additions & 0 deletions tests/scanners/test_gguf_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2534,6 +2534,51 @@ def test_ggml_variant_scanner_basic(tmp_path):
assert result.metadata.get("magic") == "GGMF"


@pytest.mark.parametrize(
("magic", "suffix"),
[
(b"GGML", ".ggml"),
(b"GGMF", ".ggmf"),
(b"GGJT", ".ggjt"),
(b"GGLA", ".ggla"),
(b"GGSA", ".ggsa"),
],
ids=["ggml", "ggmf", "ggjt", "ggla", "ggsa"],
)
def test_ggml_scanner_inspects_embedded_zip_polyglot_members(
tmp_path: Path,
magic: bytes,
suffix: str,
) -> None:
path = tmp_path / f"polyglot{suffix}"
_write_ggml_variant_file(path, magic)
pickle_path = create_malicious_pickle(tmp_path / "payload.pkl")
_append_gguf_zip(path, {"payload.pkl": pickle_path.read_bytes(), "../escaped.txt": b"escape"})

direct = GgufScanner().scan(str(path))
aggregate = scan_model_directory_or_file(str(path), cache_enabled=False)

for result in (direct, aggregate):
assert any(issue.rule_code == "S908" and issue.severity == IssueSeverity.CRITICAL for issue in result.issues)
assert any(issue.rule_code == "S201" and "system" in issue.message.lower() for issue in result.issues)
assert any(issue.rule_code == "S405" and "escaped.txt" in issue.message for issue in result.issues)
assert any(check.name == "GGML ZIP Polyglot Detection" for check in direct.checks)
assert determine_exit_code(aggregate) == 1


def test_ggml_scanner_does_not_misclassify_invalid_zip_near_match(tmp_path: Path) -> None:
path = tmp_path / "zip-near-match.ggml"
_write_ggml_file(path)
with path.open("ab") as handle:
handle.write(b"PK\x03\x04not-a-valid-archive")

result = GgufScanner().scan(str(path))

assert result.success is True
assert not any(issue.rule_code == "S908" for issue in result.issues)
assert not any(issue.severity == IssueSeverity.CRITICAL for issue in result.issues)


def test_ggml_scanner_suspicious_version(tmp_path):
"""Test that GGML scanner handles unusual versions gracefully."""
path = tmp_path / "unusual_version.ggml"
Expand Down
Loading