Skip to content

Hardening security and stability - #65

Merged
skupriienko-mailgun merged 29 commits into
mainfrom
fix/v1.9.1-hardening-and-stability
Sep 17, 2026
Merged

skupriienko-mailgun merged 29 commits into
mainfrom
fix/v1.9.1-hardening-and-stability

Conversation

@skupriienko-mailgun

@skupriienko-mailgun skupriienko-mailgun commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator
Links:

Jira

Actions:
  • Security & Guardrails:

    • SSRF Scheme & Host Whitelisting (CWE-918): Enforced strict ALLOWED_SCHEMES validation (https, http) in SecurityGuard.validate_mailgun_url() and extended trusted host rules to cover .mailgun.com.
    • Multi-Pass URL Encoding & Traversal Defense (CWE-116 / CWE-22): Hardened SecurityGuard.sanitize_domain() to recursively evaluate unquote() across up to 3 passes, apply NFKC Unicode normalization, and strip CRLF/slash patterns prior to evaluating directory traversal sequences (..).
    • Webhook Replay Attack TTL Verification (CWE-294): Updated default signature verification TTL in SecurityGuard.verify_webhook() to 900 seconds (15 minutes), rejecting expired requests while allowing <= 0 to bypass timestamp evaluation during static unit testing.
    • Pre-Flight Deliverability & XSS Protection (CWE-79 / CWE-400): Extended SpamGuard with _BLOCKED_TAGS (iframe, object, embed, applet) and automated regex detection for inline DOM event handlers (on*), adding pre-parsing payload length validation prior to feeding the HTML parser.
    • Deep Log Redaction & Tuple Structure Preservation (CWE-316 / CWE-117): Increased MAX_REDACTION_DEPTH to 5 in RedactingFilter and added frozenset support. Preserved record.args types (tuple, dict, list) to prevent string formatting crashes, wrapped object inspection in defensive guards, and provided fallback list representations when redacted set elements become unhashable.
    • Payload Recursion & Cycle Traps: Added _deep_sanitize() with a 50-level depth threshold to IdempotencyGuard to eliminate circular reference crashes and recursion overflows during fingerprint calculations.
    • Header Injection Mitigation (CWE-113 / RFC 9110): Enforced control character validation across header keys and values in SecurityGuard.sanitize_headers(), guarding sys.audit telemetry invocations with sys in sys.modules.
  • Bug Fixes & Network Resilience:

    • Attachment Stream Pointer Restoration: Fixed IdempotencyGuard.generate_key() to record stream.tell() before reading file payloads and restore the stream offset via seek() after hashing, preventing stream exhaustion during retries.
    • ChunkedStreamer Seeking & Pointer Handling: Implemented seek() and tell() on ChunkedStreamer, enforced strictly positive chunk_size bounds, and introduced explicit _eof state tracking to prevent duplicate reads.
    • Stream Pagination Type Drift & Malformed Payload Guards: Added _cast_query_param() to preserve user filter collection types (int, float, bool, list, tuple, set) during cursor pagination, and added guards for missing next links or non-dictionary API responses.
    • Builder JSON Serialization: Added default=str to json.dumps() across MailgunMessageBuilder, MailgunTemplateBuilder, and BaseEndpoint to prevent serialization crashes on custom objects (UUID, datetime).
    • Message Builder File List Immutability: Returned shallow copies of internal file lists (list(self._files)) in MailgunMessageBuilder.build() to prevent external consumers from mutating internal builder state.
    • Internal Attribute Dispatch Isolation: Explicitly blocked dynamic routing resolution for config and auth in Client.__getattr__(), raising AttributeError instead of constructing bad endpoint paths.
    • Domain Route Alias Canonicalization: Normalized domain route aliases (DOMAIN_ALIASES) directly in handle_domains() and preserved literal @ delimiters during credential path generation.
    • Timeout Float Capacity Overflow: Added explicit OverflowError handling during float conversion in SecurityGuard.sanitize_timeout(), mapping out-of-capacity numbers to standard ValueError exceptions.
    • Punycode Host Normalization: Updated SecurityGuard.normalize_domain() to partition email addresses and apply IDNA Punycode encoding specifically to the domain segment.
  • Architecture & Developer Experience (DX):

    • Route Registry Expansion: Registered routes_match under v3 endpoint routing, mapped reputationanalytics_v2 under v2 prefix routes, and registered deprecation warnings for the legacy v1/spamtraps API family[.
    • Dependency Alignment: Aligned runtime and development requirements to requests >=2.33.0, decoupled channel-pinned conda-forge::httpx2, and synchronized .pre-commit-config.yaml dependency constraints.
    • Ruff Lint Configuration: Removed the deprecated missing-trailing-comma selector from pyproject.toml to restore mdformat-ruff compatibility.
  • Testing, CI/CD & Benchmarks:

    • Stateful Fuzzing: Extended fuzz_stateful_client.py with attachment seek pointer validation, circular custom variable injection, null cursor pagination shocks, and timeout boundary tests.
    • Property Invariants: Expanded tests/property/tests.py with invariant checks for idempotency seek pointers, redacting filter args preservation, path segment sanitization idempotency, and webhook replay windows.
    • Regression Suite: Added tests covering libFuzzer crash artifacts and exploding __repr__/__str__ custom objects in test_regression.py.
    • Performance Benchmarks: Documented cold-boot profile gains (~40.5% faster startup) and verified zero regression across hot-path routing (~1.05M OPS) in PERFORMANCE.md.

Verification & Testing:

To verify these changes locally, ensure your environment variables (APIKEY and DOMAIN, and others) are set, then run the following commands:

1. Run the Unit Test Suite (Fast):
Validates the core routing logic, new guardrails (SpamGuard, IdempotencyGuard), RetryPolicy, and strict Pydantic payload schemas.

pytest tests/unit/ -v

2. Run the Live Routing Meta-Test (No state mutation):
Proves the SDK correctly constructs URLs for all supported endpoints by hitting live Mailgun servers (expects 200, 400, 401, or 403 responses; tests fail if the Python SDK crashes or generates a 404 bad route).

pytest tests/integration/test_routing_meta_live.py -v -s

3. Run the Full Integration Suite (State mutation):
Executes end-to-end flows against your Sandbox domain (creates/deletes real resources).

pytest tests/integration/test_integration_sync.py -v
pytest tests/integration/test_integration_async.py -v

4. Execute the Interactive Smoke Test:
Runs the executable documentation script demonstrating cross-version routing and payload serialization.

python mailgun/examples/smoke_test.py

5. Run the Fuzzing Suite (Security Saturation):
Executes Atheris mutation coverage across core handlers, parsers, and client lifecycles.

bash manage.sh fuzz_all 3600 -use_value_profile=1 -max_len=4096 -shrink=1

6. Run the Performance & Cold-Boot Benchmarks:
Validates the new O(1) routing dispatch and __slots__ memory optimizations using our unified DX script.

./manage.sh perf_profile
./manage.sh perf_bench

…hing

- Neutralize multi-pass URL encoding bypasses (CWE-116) and catch timeout float overflows.
- Add _deep_sanitize to IdempotencyGuard against cyclic references and recursion overflows.
- Preserve and restore file stream seek pointers during payload fingerprint hashing.
- Block executable tags and inline event handlers in SpamGuard.
- Update unit tests covering proxy initialization, overflow validation, and tag blocks.
…mat injection

- Fallback to list representation when redacted set elements become unhashable.
- Wrap custom object attribute inspection in defensive exception boundaries.
- Preserve record.args tuple, dict, and list structures to prevent string formatting crashes.
- Increase MAX_REDACTION_DEPTH to 5 and add support for frozenset.
- Add regression tests covering libFuzzer format operator payloads.
…s safely

- Implement seek() and tell() on ChunkedStreamer with explicit EOF state tracking.
- Validate that chunk_size is a strictly positive integer.
- Return a shallow copy of files from MailgunMessageBuilder.build().
- Add default=str to json.dumps across message and template builders.
- Add unit tests verifying ChunkedStreamer EOF, seek/tell, and invalid chunk sizes.
…attributes

- Canonicalize domain aliases via DOMAIN_ALIASES and preserve literal '@' in credentials logins.
- Explicitly block dynamic attribute lookups for 'config' and 'auth' in Client.__getattr__.
- Add unit test asserting Client.__getattr__ raises AttributeError on internal config and auth lookups.
…eam pagination

- Retain original filter types (tuple, set, list, int, float, bool) across cursor iterations in Endpoint and AsyncEndpoint.
- Stop pagination cleanly when response items are empty or response payload is not a dictionary.
- Add unit tests for synchronous and asynchronous stream pagination type casting and non-dict response guards.
- Guard route deletion against empty items list to avoid IndexError on clean test domains.
- Safely parse sender address in sync route tests via email.utils.parseaddr.
- Add region_data assertion to sync and async user payload tests.
…le invariants

- Add property tests for IdempotencyGuard stream seek pointer restoration.
- Add property tests for RedactingFilter args type preservation during string formatting.
- Add webhook replay TTL window property tests.
- Implement MailgunStateSequenceMachine for end-to-end stateful lifecycle testing.
…ful fuzzer

- Append LibFuzzer dictionary tokens targeting XML/CDATA, formatting specifiers, and encodings.
- Add stream pointer verification, cyclic variable payloads, null paging cursors, and timeout chaos actions to fuzz_stateful_client.
…object attributes

- Wrap dict.items() and record.__dict__.keys() in list() calls during deep redaction to prevent dictionary modification errors during iteration.

- Guard model_dump and __dict__ attribute access in defensive try-except blocks to catch dynamic property lookup failures.

- Return safe fallback directly when str(data) raises during stringification.

- Add regression tests for crashing fuzz payloads and exploding __repr__/__str__ objects.
- Append malformed HTML doctype tokens and crash reproducer byte sequences to fuzz.dict.
@skupriienko-mailgun skupriienko-mailgun self-assigned this Sep 11, 2026
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/regression/test_regression.py Fixed
Comment thread tests/regression/test_regression.py Fixed
Comment thread tests/regression/test_regression.py Fixed
Comment thread tests/regression/test_regression.py Fixed
Comment thread tests/property/tests.py Fixed
Comment thread tests/property/tests.py Fixed
Comment thread tests/property/tests.py Fixed
Comment thread tests/property/tests.py Fixed
Comment thread tests/property/tests.py Fixed
Comment thread tests/property/tests.py Fixed
Comment thread tests/property/tests.py Fixed
Comment thread tests/property/tests.py Fixed
… domain sanitization

- Convert raw HTTPStatusError into ApiError during async lazy pagination.
- Enforce ISO-8859-1 (Latin-1) wire encoding and CRLF boundary checks in SecurityGuard.sanitize_headers().
- Reject domains with null bytes or unhandled control characters in SecurityGuard.normalize_domain().
- Harden handle_address_validate() against non-sequence URL key payloads.
- Add field validator on subject to reject CRLF injection sequences (CWE-113).
- Clean up redundant comments and streamline model configuration.
…y seeds

- Add 5-second execution timeout cap to manage.sh fuzz_all().
- Update fuzz.dict with newly harvested edge cases and control byte tokens.
- Add concurrency and connection pool stress testing to fuzz_async_client.py.
- Prevent exponential backoff blocking and handle Retry-After edge cases in fuzz_async_evil_server.py.
- Modernize test harnesses across builders, handlers, endpoints, webhooks, and stateful clients.
…sources

- Set reportMissingModuleSource to none in pyproject.toml.
- Silence stub warnings for untyped third-party packages in pre-commit environments.
- Document frozenset support in RedactingFilter.
- Note requests >=2.33.0 dependency synchronization.
…ork:mailgun/mailgun-python into fix/v1.9.1-hardening-and-stability
Comment thread tests/fuzz/fuzz_async_client.py Fixed
Comment thread tests/fuzz/fuzz_audit_events.py Fixed
Comment thread tests/fuzz/fuzz_audit_events.py Fixed
Comment thread tests/fuzz/fuzz_builders.py Fixed
Comment thread tests/fuzz/fuzz_builders_advanced.py Fixed
Comment thread tests/fuzz/fuzz_builders_advanced.py Fixed
Comment thread tests/fuzz/fuzz_builders_advanced.py Fixed
Comment thread tests/fuzz/fuzz_builders_advanced.py Fixed
Comment thread tests/fuzz/fuzz_error_parser.py Fixed
Comment thread tests/fuzz/fuzz_pagination.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_stateful_client.py Fixed
Comment thread tests/fuzz/fuzz_webhooks.py Fixed
Comment thread tests/fuzz/stateful_async_client.py Fixed
Comment thread tests/fuzz/stateful_async_client.py Fixed
Comment thread tests/fuzz/stateful_async_client.py Fixed
Comment thread tests/fuzz/fuzz_semantic_payloads.py Fixed
Comment thread tests/fuzz/fuzz_log_redaction.py Fixed
Comment thread tests/fuzz/fuzz_audit_events.py Fixed
Comment thread tests/fuzz/fuzz_builders_advanced.py Fixed
Comment thread tests/fuzz/fuzz_pagination.py Fixed
Comment thread tests/fuzz/fuzz_pydantic_models.py Fixed
@skupriienko-mailgun
skupriienko-mailgun marked this pull request as ready for review September 17, 2026 15:40
@skupriienko-mailgun
skupriienko-mailgun merged commit e0905ee into main Sep 17, 2026
23 checks passed
@skupriienko-mailgun
skupriienko-mailgun deleted the fix/v1.9.1-hardening-and-stability branch September 17, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant