September 4, 2021·4 min read·engineeringpythonsmtpsecurity

Python 3.7.12 rejects newlines in SMTP commands

Python 3.7.12 checks assembled SMTP commands for carriage returns and line feeds. The security fix keeps argument text from creating another protocol command.

A string passed as a protocol argument must remain an argument when the library writes it to the connection. A newline can violate that requirement in a line-oriented protocol. Python 3.7.12, released on September 4, includes a correction to smtplib that rejects carriage returns and line feeds in assembled commands. The fix acts at the point where command names and arguments become wire data.

The release changelog identifies issue 43124 and describes the command-injection possibility as unlikely. That qualification should remain part of the explanation. The library defect does not establish that every mail-sending application supplies attacker-controlled command arguments, but it removes a condition that could let such arguments change the protocol conversation.

I value Python libraries that provide a clear boundary between application data and protocol construction. An application should still decide which values belong in a command. The library can enforce the simpler invariant that one command submission cannot contain an additional line supplied through its arguments.

The helper constructs the command line

The versioned smtplib implementation shows putcmd combining a command and its optional arguments. When arguments exist, the helper places a space between them and the command. It then appends the protocol's carriage-return and line-feed terminator before sending the result.

Without a check before that final send, an embedded terminator can introduce a new line inside the constructed command. Text after it can then reach the server in a different syntactic position from the one the caller intended. This is a serialization problem. A valid argument string and a valid complete protocol line have different requirements.

The fix checks the assembled text for either carriage return or line feed. If either exists, it raises ValueError before calling send. Checking the combined string covers the command name as well as its arguments. It also avoids relying on each public method to reproduce the same delimiter check independently.

The implementation escapes those characters in the diagnostic string before raising the exception. That escaping is for the error message, not a way to transmit a modified command. The rejected input does not become a command with newline characters silently removed or replaced. The caller receives an error and must decide how to handle the invalid value.

Public operations share the helper

The SMTP API documentation for this version describes docmd as sending a command with optional arguments and returning the server's response. It is a lower-level facility than ordinary mail sending and can support private extensions. The implementation calls putcmd and then getreply.

Other methods also use putcmd, including the greeting operations. Centralizing rejection here gives those paths the same rule before bytes leave the client. A caller's choice of a higher-level method does not require a separate assumption that the shared helper will accept embedded line delimiters safely.

The regression test included in the release's SMTP test file submits a command containing a newline and expects ValueError. It also checks the diagnostic text. This test targets the rejected input directly, so its success does not depend on persuading a real mail server to perform a consequential injected operation.

That distinction is useful for understanding the fix. A protocol-level exploit would involve server state and application-specific authority. The library's invariant can be tested earlier, at command construction, where the delimiter is unambiguously invalid for this helper.

Rejection is specific to command construction

The source keeps sending command lines separate from sending message data. Its send method has a comment explaining that data handling has its own encoding requirements. The new check belongs in putcmd because it is that helper's responsibility to assemble one command line.

This does not imply that line breaks are invalid everywhere in email. It means the text provided as a single command or its arguments cannot contain characters that create additional protocol lines. Applying the same rejection indiscriminately to message content would confuse two different syntactic roles.

Applications that accidentally supplied such characters now encounter an explicit failure. Catching that failure and retrying the same malformed string does not change its meaning. A useful response is to identify which input supplied the delimiter and whether that field should have accepted multiline text at all.

I prefer the explicit exception to an attempted repair of the submitted command. Removing delimiters could produce a different argument with an unintended meaning. Rejecting the input preserves the fact that the caller supplied something outside the helper's accepted grammar.

Python 3.7.12 gives SMTP command construction a direct delimiter check before transmission. The application still controls the command's meaning and the server still controls its response, but an embedded carriage return or line feed can no longer pass through this helper as ordinary command text.