ποΈ(pymta) harden pymta with new settings & limits - #777
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. π βΉοΈ Recent review infoβοΈ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: π Files selected for processing (1)
π WalkthroughWalkthroughThe pure-Python MTA validates settings, enforces trusted PROXY use, applies SMTP session and DATA deadlines, preserves counters across STARTTLS, and classifies MDA responses with configurable JWT TTL rules. Documentation and tests cover these changes. ChangesSMTP and MDA hardening
Estimated code review effort: 4 (Complex) | ~45 minutes Mergeability Score: βͺ Minimal Β· up to The current changes introduce no actionable merge-blocking risk based on the supplied evidence and are merge-ready after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant SMTPClient
participant HardenedSMTP
participant SMTPHandler
participant MDAClient
participant Transport
SMTPClient->>HardenedSMTP: Establish SMTP session
HardenedSMTP->>Transport: Arm session deadline
SMTPClient->>SMTPHandler: Send SMTP commands and DATA
SMTPHandler->>MDAClient: Deliver within remaining DATA budget
MDAClient-->>SMTPHandler: Return delivery result
SMTPHandler-->>HardenedSMTP: Return reply and disconnect request
HardenedSMTP->>Transport: Flush reply and close when requested
Possibly related PRs
Suggested reviewers: π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touches π‘ 1π Generate docstrings π‘
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/mta-in/README.md`:
- Around line 49-51: Update the Authorization documentationβs body_hash
description to remove the βreplay-proof per-requestβ claim, stating only that it
binds the token to the exact request bytes. Do not add replay tracking or other
implementation changes unless the documentation is intended to guarantee replay
prevention.
In `@src/mta-in/src/pymta/handler.py`:
- Around line 274-286: Update handle_MAIL() to count malformed sender addresses
and invalid or oversized SIZE parameters, then apply the shared
PYMTA_HARD_ERROR_LIMIT 421-and-disconnect guard before each soft-error return.
Reuse the existing guard behavior from handle_RCPT() and preserve the current
501 responses for individual MAIL validation failures.
- Around line 142-162: Update _remaining_data_budget so it never floors a
depleted budget to one second: after subtracting spent time and
_REPLY_RESERVE_SECONDS from settings.PYMTA_DATA_TIMEOUT, return the actual
nonnegative remaining value or raise TimeoutError when it is nonpositive,
allowing the existing timeout path to send 451 before the DATA deadline.
In `@src/mta-in/src/pymta/mda_async.py`:
- Around line 44-62: Scope _PERMANENT_STATUSES handling in _post to delivery
requests only, so check_recipient responses with 400, 413, or 415 defer instead
of producing temp_fail=False; preserve deferral for other unexpected
recipient-check responses. Update permanent-status tests to exercise deliver and
add check_recipient coverage verifying these statuses defer.
In `@src/mta-in/src/pymta/server.py`:
- Around line 53-59: Update the startup validation guarding
PYMTA_TRUSTED_PROXIES to reject entries representing zero-prefix networks,
including 0.0.0.0/0 and ::/0, in addition to an empty allowlist. Preserve the
existing RuntimeError behavior and ensure validation occurs before the server
accepts connections.
In `@src/mta-in/tests/test_settings.py`:
- Around line 76-79: Update _env_bool so unrecognized non-empty environment
values raise ValueError instead of silently returning the supplied default.
Revise test_bool_unrecognised_value_keeps_the_default to assert the exception
for both default values, while preserving recognized-value parsing and
missing-variable defaults.
πͺ Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9365f2bf-db5a-4c25-97bd-6bc7bb679c00
π Files selected for processing (16)
deploy/env/mta-in-py.defaultssrc/mta-in/README.mdsrc/mta-in/src/pymta/address.pysrc/mta-in/src/pymta/controller.pysrc/mta-in/src/pymta/handler.pysrc/mta-in/src/pymta/limits.pysrc/mta-in/src/pymta/mda_async.pysrc/mta-in/src/pymta/metrics.pysrc/mta-in/src/pymta/server.pysrc/mta-in/src/pymta/settings.pysrc/mta-in/src/pymta/smtp_protocol.pysrc/mta-in/tests/test_handler.pysrc/mta-in/tests/test_hardened_smtp.pysrc/mta-in/tests/test_mda_async.pysrc/mta-in/tests/test_server_config.pysrc/mta-in/tests/test_settings.py
| def test_bool_unrecognised_value_keeps_the_default(monkeypatch): | ||
| monkeypatch.setenv("PYMTA_TEST_BOOL", "maybe") | ||
| assert _env_bool("PYMTA_TEST_BOOL", True) is True | ||
| assert _env_bool("PYMTA_TEST_BOOL", False) is False |
There was a problem hiding this comment.
π Security & Privacy | π Major | β‘ Quick win
Reject unrecognized Boolean values.
This test requires invalid configuration to fall back silently. It contradicts the fail-fast contract on Lines 3-4.
For example, PYMTA_ENABLE_PROXY_PROTOCOL=treu uses the default False. A deployment behind a PROXY-enabled load balancer then attributes all sessions to the load balancer.
Update _env_bool to raise ValueError, and require that behavior here.
Proposed test change
-def test_bool_unrecognised_value_keeps_the_default(monkeypatch):
+def test_bool_rejects_unrecognised_value(monkeypatch):
monkeypatch.setenv("PYMTA_TEST_BOOL", "maybe")
- assert _env_bool("PYMTA_TEST_BOOL", True) is True
- assert _env_bool("PYMTA_TEST_BOOL", False) is False
+ with pytest.raises(ValueError, match="must be a boolean"):
+ _env_bool("PYMTA_TEST_BOOL", False)π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mta-in/tests/test_settings.py` around lines 76 - 79, Update _env_bool so
unrecognized non-empty environment values raise ValueError instead of silently
returning the supplied default. Revise
test_bool_unrecognised_value_keeps_the_default to assert the exception for both
default values, while preserving recognized-value parsing and missing-variable
defaults.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and canβt be posted inline due to platform limitations.
β οΈ Outside diff range comments (1)
src/mta-in/README.md (1)
29-42: π Maintainability & Code Quality | π‘ Minor | β‘ Quick winScope the outcome table to
deliver.The table states that HTTP 400, 413, and 415 always produce
554.check_recipientdefers these statuses and the SMTP handler returns451.Mark this table as the
deliveroutcome mapping. Document that recipient checks defer these statuses.Proposed documentation change
-### Translating the MDA outcome +### Translating the MDA delivery outcome + +For `check/`, HTTP 400, 413, and 415 are temporary failures. They produce +SMTP `451` because a recipient check contains no message to reject. -Losing a legitimate message is worse than asking the sender to retry, so the permanent-rejection set is an explicit allow-list and everything else defers: +For `deliver/`, losing a legitimate message is worse than asking the sender to retry, so the permanent-rejection set is an explicit allow-list and everything else defers:π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mta-in/README.md` around lines 29 - 42, Update the βTranslating the MDA outcomeβ section to explicitly scope its outcome table to deliver responses. Add documentation stating that check_recipient defers 400, 413, and 415, so its SMTP handler returns 451 rather than applying the tableβs 554 mapping.
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/mta-in/README.md`:
- Around line 29-42: Update the βTranslating the MDA outcomeβ section to
explicitly scope its outcome table to deliver responses. Add documentation
stating that check_recipient defers 400, 413, and 415, so its SMTP handler
returns 451 rather than applying the tableβs 554 mapping.
βΉοΈ Review info
βοΈ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: de4db120-4f89-4d92-8686-a1726bde25f0
π Files selected for processing (13)
src/mta-in/README.mdsrc/mta-in/pyproject.tomlsrc/mta-in/src/delivery_milter.pysrc/mta-in/src/pymta/handler.pysrc/mta-in/src/pymta/mda_async.pysrc/mta-in/src/pymta/server.pysrc/mta-in/src/pymta/settings.pysrc/mta-in/tests/test_handler.pysrc/mta-in/tests/test_limits.pysrc/mta-in/tests/test_mda_async.pysrc/mta-in/tests/test_security.pysrc/mta-in/tests/test_server_config.pysrc/mta-in/tests/test_settings.py
There was a problem hiding this comment.
Actionable comments posted: 1
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/mta-in/src/pymta/server.py`:
- Around line 56-77: Reject startup in the server configuration validation when
PROXY protocol is enabled and PYMTA_TRUSTED_PROXIES is empty or contains a
zero-prefix network, raising RuntimeError instead of warning. In
src/mta-in/src/pymta/handler.py lines 194-202, return False for an empty
allowlist; update src/mta-in/tests/test_server_config.py lines 26-64 and
src/mta-in/tests/test_handler.py lines 632-644 to assert rejection; document the
explicit trusted proxy IP/CIDR requirement at src/mta-in/README.md line 111 and
remove the unspecified trust-boundary topology at lines 131-140.
πͺ Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a04f3651-5248-4f1b-897c-550c63f8c90b
π Files selected for processing (7)
src/mta-in/README.mdsrc/mta-in/src/pymta/handler.pysrc/mta-in/src/pymta/mda_async.pysrc/mta-in/src/pymta/server.pysrc/mta-in/tests/test_handler.pysrc/mta-in/tests/test_mda_async.pysrc/mta-in/tests/test_server_config.py
| if not settings.PYMTA_ENABLE_PROXY_PROTOCOL: | ||
| return | ||
| # A zero-prefix network (0.0.0.0/0, ::/0) matches every peer, so it is the | ||
| # empty allowlist wearing a disguise. Same posture, same warning. | ||
| catch_all = [net for net in settings.PYMTA_TRUSTED_PROXIES if net.prefixlen == 0] | ||
| if not settings.PYMTA_TRUSTED_PROXIES or catch_all: | ||
| why = ( | ||
| "PYMTA_TRUSTED_PROXIES is empty" | ||
| if not settings.PYMTA_TRUSTED_PROXIES | ||
| else f"PYMTA_TRUSTED_PROXIES contains {', '.join(str(n) for n in catch_all)}, " | ||
| "which matches every peer" | ||
| ) | ||
| logger.warning( | ||
| "SECURITY: PROXY protocol is enabled but %s, so a PROXY header is trusted " | ||
| "from any peer. Any host able to reach port %s directly can forge its " | ||
| "source IP past the per-IP caps and into the Received header. Set it to " | ||
| "the load balancer's IPs/CIDRs, and make sure the port is reachable only " | ||
| "from the balancer.", | ||
| why, | ||
| settings.PYMTA_SMTP_PORT, | ||
| ) | ||
| return |
There was a problem hiding this comment.
π Security & Privacy | π Major | β‘ Quick win
Reject PROXY protocol without a specific trusted-peer allowlist.
An empty or catch-all allowlist authorizes any host that can reach the SMTP listener to forge src_addr. That forged address bypasses per-IP limits and enters the MDA Received metadata. A warning does not establish the required network boundary.
src/mta-in/src/pymta/server.py#L56-L77: raiseRuntimeErrorwhenPYMTA_TRUSTED_PROXIESis empty or contains a zero-prefix network.src/mta-in/src/pymta/handler.py#L194-L202: returnFalsefor an empty allowlist as defense in depth.src/mta-in/tests/test_server_config.py#L26-L64: change warning tests to require startup rejection.src/mta-in/tests/test_handler.py#L632-L644: require an empty allowlist to reject the PROXY header.src/mta-in/README.md#L111-L111: state that PROXY mode requires explicit trusted proxy IPs or CIDRs.src/mta-in/README.md#L131-L140: remove the topology that permits an unspecified proxy trust boundary.
π§° Tools
πͺ GitHub Check: CodeQL
[failure] 74-74: Clear-text logging of sensitive information
This expression logs sensitive data (secret) as clear text.
π Affects 5 files
src/mta-in/src/pymta/server.py#L56-L77(this comment)src/mta-in/src/pymta/handler.py#L194-L202src/mta-in/tests/test_server_config.py#L26-L64src/mta-in/tests/test_handler.py#L632-L644src/mta-in/README.md#L111-L111src/mta-in/README.md#L131-L140
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mta-in/src/pymta/server.py` around lines 56 - 77, Reject startup in the
server configuration validation when PROXY protocol is enabled and
PYMTA_TRUSTED_PROXIES is empty or contains a zero-prefix network, raising
RuntimeError instead of warning. In src/mta-in/src/pymta/handler.py lines
194-202, return False for an empty allowlist; update
src/mta-in/tests/test_server_config.py lines 26-64 and
src/mta-in/tests/test_handler.py lines 632-644 to assert rejection; document the
explicit trusted proxy IP/CIDR requirement at src/mta-in/README.md line 111 and
remove the unspecified trust-boundary topology at lines 131-140.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation