From dc2da67fb3dcd4f47e985ece5a142b7c7dafe842 Mon Sep 17 00:00:00 2001 From: Username Date: Fri, 26 Dec 2025 17:09:02 +0100 Subject: [PATCH] add Hypothesis property-based MIME detection tests - test_magic_prefix_detection: verify all signatures with random suffix - test_random_binary_never_crashes: random data never crashes - test_partial_magic_no_false_match: truncated magic handled safely - test_magic_not_at_start_ignored: only detect magic at offset 0 --- documentation/security-testing-status.md | 8 +- tests/test_fuzz.py | 102 +++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/documentation/security-testing-status.md b/documentation/security-testing-status.md index b5c9ad9..6af4ee3 100644 --- a/documentation/security-testing-status.md +++ b/documentation/security-testing-status.md @@ -155,11 +155,17 @@ Not tested (no signature defined): ``` [ ] Add --target option to run_fuzz.py for external testing [ ] Implement adaptive rate limiting in production fuzzer -[ ] Add hypothesis property-based tests for MIME detection +[x] Add hypothesis property-based tests for MIME detection [ ] Create polyglot generator for automated MIME confusion testing [x] Add timing attack tests for authentication endpoints ``` +**Hypothesis MIME Tests (2025-12-26):** +- `test_magic_prefix_detection`: All known signatures + random suffix detect correctly +- `test_random_binary_never_crashes`: Random binary never crashes detector +- `test_partial_magic_no_false_match`: Truncated magic bytes handled safely +- `test_magic_not_at_start_ignored`: Magic at non-zero offset ignored + ### Penetration Testing (from PENTEST_PLAN.md) ``` diff --git a/tests/test_fuzz.py b/tests/test_fuzz.py index 3828261..243e09a 100644 --- a/tests/test_fuzz.py +++ b/tests/test_fuzz.py @@ -168,6 +168,108 @@ class TestMimeTypeFuzzing: pass +class TestMimeDetectionFuzzing: + """Property-based tests for MIME magic byte detection.""" + + # Known magic signatures mapped to expected MIME types + MAGIC_SIGNATURES: ClassVar[list[tuple[bytes, str]]] = [ + (b"\x89PNG\r\n\x1a\n", "image/png"), + (b"\xff\xd8\xff", "image/jpeg"), + (b"GIF87a", "image/gif"), + (b"GIF89a", "image/gif"), + (b"%PDF", "application/pdf"), + (b"PK\x03\x04", "application/zip"), + (b"\x1f\x8b", "application/gzip"), + (b"fLaC", "audio/flac"), + (b"OggS", "audio/ogg"), + (b"ID3", "audio/mpeg"), + (b"\x7fELF", "application/x-executable"), + (b"MZ", "application/x-msdownload"), + (b"BZh", "application/x-bzip2"), + (b"7z\xbc\xaf\x27\x1c", "application/x-7z-compressed"), + (b"SQLite format 3\x00", "application/x-sqlite3"), + # HEIC/HEIF/AVIF (ftyp box format) + (b"\x00\x00\x00\x18\x66\x74\x79\x70\x68\x65\x69\x63", "image/heic"), + (b"\x00\x00\x00\x18\x66\x74\x79\x70\x6d\x69\x66\x31", "image/heif"), + (b"\x00\x00\x00\x1c\x66\x74\x79\x70\x61\x76\x69\x66", "image/avif"), + ] + + @settings(max_examples=100, suppress_health_check=FIXTURE_HEALTH_CHECKS) + @given(suffix=st.binary(min_size=0, max_size=1000)) + def test_magic_prefix_detection(self, client, suffix): + """Magic bytes followed by arbitrary data should detect correctly.""" + for magic, expected_mime in self.MAGIC_SIGNATURES: + content = magic + suffix + response = client.post( + "/", + data=content, + content_type="application/octet-stream", + ) + if response.status_code == 201: + data = json.loads(response.data) + assert data["mime_type"] == expected_mime, ( + f"Expected {expected_mime} for magic {magic!r}, got {data['mime_type']}" + ) + + @settings(max_examples=200, suppress_health_check=FIXTURE_HEALTH_CHECKS) + @given(content=st.binary(min_size=1, max_size=5000)) + def test_random_binary_never_crashes(self, client, content): + """Random binary content should never crash MIME detection.""" + response = client.post( + "/", + data=content, + content_type="application/octet-stream", + ) + assert response.status_code in (201, 400, 413, 429, 503) + if response.status_code == 201: + data = json.loads(response.data) + # MIME type should always be a valid format + assert "/" in data["mime_type"] + assert len(data["mime_type"]) < 100 + + @settings(max_examples=100, suppress_health_check=FIXTURE_HEALTH_CHECKS) + @given( + magic=st.sampled_from([m for m, _ in MAGIC_SIGNATURES]), + truncate=st.integers(min_value=1, max_value=10), + ) + def test_partial_magic_no_false_match(self, client, magic, truncate): + """Truncated magic bytes should not produce false positive matches.""" + if truncate >= len(magic): + return # Skip if we'd use full magic + partial = magic[:truncate] + # Add random suffix that's clearly not the rest of the magic + content = partial + b"\xff\xfe\xfd\xfc" * 10 + + response = client.post( + "/", + data=content, + content_type="application/octet-stream", + ) + # Partial magic should not crash - may match different signature or fallback + assert response.status_code in (201, 400, 413, 429, 503) + + @settings(max_examples=50, suppress_health_check=FIXTURE_HEALTH_CHECKS) + @given( + content=st.binary(min_size=100, max_size=1000), + inject_pos=st.integers(min_value=20, max_value=80), + ) + def test_magic_not_at_start_ignored(self, client, content, inject_pos): + """Magic bytes not at offset 0 should not trigger detection.""" + # Inject PNG magic in middle of random data + png_magic = b"\x89PNG\r\n\x1a\n" + if inject_pos < len(content): + modified = content[:inject_pos] + png_magic + content[inject_pos:] + response = client.post( + "/", + data=modified, + content_type="application/octet-stream", + ) + if response.status_code == 201: + data = json.loads(response.data) + # Should NOT detect as PNG (magic not at start) + assert data["mime_type"] != "image/png" + + class TestJsonFuzzing: """Fuzz JSON endpoint handling."""