ποΈ(pymta) harden pymta with new settings & limits - #777
Conversation
|
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 now validates settings, supports trusted PROXY networks, enforces SMTP session and DATA deadlines, preserves counters across STARTTLS, and applies explicit MDA response and JWT TTL rules. Documentation and tests cover the new behavior. ChangesSMTP and MDA hardening
Estimated code review effort: 4 (Complex) | ~45 minutes 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
π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touches π‘ 2π Generate docstrings π‘
π οΈ Fix failing CI checks π‘
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