diff --git a/backend/services/email_client.py b/backend/services/email_client.py index db17eb77a..8763a41aa 100644 --- a/backend/services/email_client.py +++ b/backend/services/email_client.py @@ -64,6 +64,10 @@ class SmtpConfig: def generate_oauth2_string(user: str, access_token: str) -> bytes: """Generates an OAuth2 string for IMAP/SMTP authentication.""" + if "\x01" in user or "\x01" in access_token: + raise ValueError( + "OAuth2 authentication fields must not contain SASL delimiters" + ) auth_string = f"user={user}\x01auth=Bearer {access_token}\x01\x01" return base64.b64encode(auth_string.encode("utf-8")) diff --git a/backend/tests/test_email_client.py b/backend/tests/test_email_client.py index ad8260c53..550e20c18 100644 --- a/backend/tests/test_email_client.py +++ b/backend/tests/test_email_client.py @@ -18,6 +18,21 @@ def test_generate_oauth2_string(): assert b"auth=Bearer dummy_token" in decoded +@pytest.mark.parametrize( + ("user", "access_token"), + [ + ("victim@example.com\x01auth=Bearer attacker", "valid_token"), + ("victim@example.com", "valid_token\x01user=attacker@example.com"), + ], +) +def test_generate_oauth2_string_rejects_sasl_field_delimiters(user, access_token): + with pytest.raises( + ValueError, + match="OAuth2 authentication fields must not contain SASL delimiters", + ): + generate_oauth2_string(user, access_token) + + def test_build_email_message_sets_reply_headers(): params = EmailMessageParams( to_address="test@example.com", diff --git a/docs/research/email-authentication-xoauth2/README.md b/docs/research/email-authentication-xoauth2/README.md new file mode 100644 index 000000000..d764ddf66 --- /dev/null +++ b/docs/research/email-authentication-xoauth2/README.md @@ -0,0 +1,54 @@ +# Email authentication — XOAUTH2 delimiter integrity + +This note grounds Naruon's SASL XOAUTH2 payload construction at +`backend/services/email_client.py` and the hostile-input regression in +`backend/tests/test_email_client.py`. + +## Protocol boundary + +RFC 7628 defines OAuth SASL key/value fields as being separated by the octet +`%x01` (Control-A). Google's Gmail XOAUTH2 documentation uses the same wire +shape for the initial client response: one `user` field, one +`auth=Bearer ...` field, and a final empty field, each separated by Control-A. +The delimiter is therefore protocol structure, not ordinary caller-controlled +field data. + +Naruon's helper previously interpolated the supplied user identity and access +token into that attribute stream before base64 encoding. A Control-A embedded +inside either value created an additional protocol field boundary. Base64 does +not remove that ambiguity; it only encodes the already-constructed octet +sequence. + +## Decision + +`generate_oauth2_string()` rejects `\x01` in either the user identity or access +token before the SASL response is constructed. The ordinary response format is +unchanged. The function does not log credentials, repair malformed values, +percent-encode the delimiter, introduce a fallback authentication mechanism, or +broaden the allowed IMAP/SMTP destinations. + +The regression corpus covers delimiter injection through both caller-controlled +fields and preserves the existing valid-payload test. This is a structural +protocol validation rule rather than a keyword/security-score heuristic. + +## Claim boundary + +This change prevents caller data from introducing extra XOAUTH2 field +separators at this construction boundary. It does not by itself claim complete +OAuth, SASL, Gmail, IMAP, or SMTP security; token issuance, audience/scope, +transport security, server policy, credential storage, TLS identity, egress +allowlisting, and provider behavior remain separate controls. + +## References (APA 7) + +- Mills, W., Showalter, T., & Tschofenig, H. (2015). *A set of Simple + Authentication and Security Layer (SASL) mechanisms for OAuth* (RFC 7628). + RFC Editor. https://www.rfc-editor.org/rfc/rfc7628.html +- Google. (n.d.). *OAuth 2.0 mechanism*. Google Workspace. Retrieved August 14, + 2026, from https://developers.google.com/workspace/gmail/imap/xoauth2-protocol + +## Verification boundary + +The branch is not merge-ready merely because this note and the narrow fix +exist. Current-head repository CI, security, coverage, independent review, and +protected-branch gates remain authoritative.