Skip to content

fix(mail): validate UID + reject header injection; fix IPv6 CSP host#955

Merged
jaylfc merged 1 commit into
devfrom
fix/mail-security-hardening
Jun 16, 2026
Merged

fix(mail): validate UID + reject header injection; fix IPv6 CSP host#955
jaylfc merged 1 commit into
devfrom
fix/mail-security-hardening

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Fix-forward for the gitar findings on the merged promotion #952 (this code is on master).

Findings addressed

  1. Security (⚠️): IMAP UID not validated before FETCH_get_message_blocking passed the client-supplied uid path param straight into conn.uid("FETCH", uid, ...). A non-numeric value could carry extra IMAP tokens. Now validated as a bare numeric UID (_validate_uid); the ids we hand out from list_messages are always numeric UIDs, so nothing legitimate is rejected.
  2. Security: SMTP header injection via subject/to/cc_build_outgoing assigned client values directly to MIME headers. CR/LF/NUL are now rejected (_validate_header), preventing injected headers (hidden Bcc, spoofed From).
  3. Bug: _strip_port corrupts bracketed IPv6 hosts — fixed to keep everything up to the closing bracket. (Latent: _SAFE_HOST_RE already rejects colons, so this never reached the CSP output, but the helper is now correct.)

Validation failures now return 400 (MailValidationError, a base of the existing MailFolderError) instead of a generic 502.

Tests

tests/test_mail_security.py — 19 cases covering numeric-UID acceptance, UID injection rejection, header CRLF rejection, and IPv6-safe port stripping. All green (pytest tests/test_mail_security.py 19 passed).

Scope: mail_client.py, routes/mail.py, middleware/security_headers.py, one new test file.

Summary by CodeRabbit

  • Tests

    • Added comprehensive test suite for mail security validation
  • Bug Fixes

    • Fixed IPv6 address handling in proxy configuration
    • Improved input validation for mail operations with clearer error responses for invalid inputs

… CSP host

Addresses gitar findings on the mail app (now on master):
- IMAP: validate the client-supplied message UID is a bare numeric UID before
  passing it to conn.uid(FETCH, ...), closing an IMAP command-injection gap.
- SMTP: reject CR/LF/NUL in to/cc/subject before assigning them to MIME headers,
  preventing header injection (hidden Bcc, spoofed headers).
- CSP: _strip_port no longer corrupts a bracketed IPv6 host.

Validation errors surface as 400 (MailValidationError) instead of a 502.
Adds tests/test_mail_security.py (19 cases).
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e94c8a01-0bb9-4574-8d6b-1dc20c889399

📥 Commits

Reviewing files that changed from the base of the PR and between d66ec15 and 24afc71.

📒 Files selected for processing (4)
  • tests/test_mail_security.py
  • tinyagentos/mail_client.py
  • tinyagentos/middleware/security_headers.py
  • tinyagentos/routes/mail.py

📝 Walkthrough

Walkthrough

Introduces MailValidationError, _validate_uid, and _validate_header in mail_client.py to reject malformed IMAP UIDs and CRLF-injected MIME headers. Fixes _strip_port in security_headers.py for bracketed IPv6 hosts. Updates mail routes to return HTTP 400 on validation failures. Adds tests/test_mail_security.py covering all new helpers.

Changes

Mail Security Hardening & IPv6 Strip-Port Fix

Layer / File(s) Summary
Validation error types, helpers, and _strip_port IPv6 fix
tinyagentos/mail_client.py, tinyagentos/middleware/security_headers.py
MailValidationError is introduced as a ValueError subclass; MailFolderError updated to inherit from it. _UID_RE, _validate_uid, and _validate_header added to enforce numeric UID format and reject CR/LF/NUL in header values. _strip_port updated to detect [...] bracketed IPv6 input and return up to the closing ] instead of naively splitting on :.
Validation applied at IMAP fetch and MIME header construction
tinyagentos/mail_client.py
_get_message_blocking calls _validate_uid before the IMAP FETCH command. _build_outgoing calls _validate_header on To, Cc, and Subject before placing values into the MIME message.
Route error handling updated to 400 on validation errors
tinyagentos/routes/mail.py
get_message now catches MailValidationError (returning 400) instead of MailFolderError. send adds an explicit MailValidationError handler returning 400 before the existing generic 502 handler.
Security test suite
tests/test_mail_security.py
New test module with TestValidateUid, TestValidateHeader, and TestStripPort covering valid inputs, parametrized rejection cases for injection/invalid strings, and IPv4/IPv6/hostname port-stripping assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A rabbit checked the mail one day,
Found sneaky \r\n trying to play.
"No UIDs with commas," it declared,
IPv6 brackets — carefully squared!
Now headers are clean, the ports stripped right,
The warren sleeps safely through the night. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(mail): validate UID + reject header injection; fix IPv6 CSP host' clearly and concisely summarizes the three main security fixes in the changeset: UID validation, header injection prevention, and IPv6 CSP handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mail-security-hardening

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines 340 to +344
msg["From"] = cfg.email_address or cfg.username
msg["To"] = to
msg["To"] = _validate_header(to, "to")
if cc:
msg["Cc"] = cc
msg["Subject"] = subject
msg["Cc"] = _validate_header(cc, "cc")
msg["Subject"] = _validate_header(subject, "subject")

@gitar-bot gitar-bot Bot Jun 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Security: From header not validated for CRLF injection

_build_outgoing now validates To, Cc, and Subject via _validate_header, but the From header (cfg.email_address or cfg.username, mail_client.py:340) is still assigned without validation. These values originate from client input at account-creation time (AccountCreate.email_address/username in routes/mail.py). If a CR/LF/NUL ever slips into the stored account record, it would reach the MIME From header and reopen the same header-injection vector this PR closes for the other fields.

This is lower severity than the addressed cases because the values are account-config rather than per-request, and the line is outside the modified region — but applying _validate_header here (or validating at account creation) would close the gap consistently.

Validate the From header value alongside To/Cc/Subject.:

msg["From"] = _validate_header(cfg.email_address or cfg.username, "from")
msg["To"] = _validate_header(to, "to")

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 resolved / 1 findings

Hardens mail processing by validating IMAP UIDs, blocking header injection in SMTP fields, and correcting IPv6 bracket parsing. Ensure the From header is also passed through _validate_header to prevent CRLF injection.

💡 Security: From header not validated for CRLF injection

📄 tinyagentos/mail_client.py:340-344

_build_outgoing now validates To, Cc, and Subject via _validate_header, but the From header (cfg.email_address or cfg.username, mail_client.py:340) is still assigned without validation. These values originate from client input at account-creation time (AccountCreate.email_address/username in routes/mail.py). If a CR/LF/NUL ever slips into the stored account record, it would reach the MIME From header and reopen the same header-injection vector this PR closes for the other fields.

This is lower severity than the addressed cases because the values are account-config rather than per-request, and the line is outside the modified region — but applying _validate_header here (or validating at account creation) would close the gap consistently.

Validate the From header value alongside To/Cc/Subject.
msg["From"] = _validate_header(cfg.email_address or cfg.username, "from")
msg["To"] = _validate_header(to, "to")
🤖 Prompt for agents
Code Review: Hardens mail processing by validating IMAP UIDs, blocking header injection in SMTP fields, and correcting IPv6 bracket parsing. Ensure the `From` header is also passed through `_validate_header` to prevent CRLF injection.

1. 💡 Security: From header not validated for CRLF injection
   Files: tinyagentos/mail_client.py:340-344

   `_build_outgoing` now validates `To`, `Cc`, and `Subject` via `_validate_header`, but the `From` header (`cfg.email_address or cfg.username`, mail_client.py:340) is still assigned without validation. These values originate from client input at account-creation time (`AccountCreate.email_address`/`username` in routes/mail.py). If a CR/LF/NUL ever slips into the stored account record, it would reach the MIME `From` header and reopen the same header-injection vector this PR closes for the other fields.
   
   This is lower severity than the addressed cases because the values are account-config rather than per-request, and the line is outside the modified region — but applying `_validate_header` here (or validating at account creation) would close the gap consistently.

   Fix (Validate the From header value alongside To/Cc/Subject.):
   msg["From"] = _validate_header(cfg.email_address or cfg.username, "from")
   msg["To"] = _validate_header(to, "to")

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@@ -306,10 +338,10 @@ def _build_outgoing(
) -> MIMEMultipart:
msg = MIMEMultipart()
msg["From"] = cfg.email_address or cfg.username

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: From header is not validated for CRLF/NUL injection

cfg.email_address and cfg.username are account configuration values supplied by the user and later used in MIME/SMTP commands. Since To, Cc, and Subject are now validated, leaving From unchecked leaves the same header-injection class available if a stored account value contains CR/LF/NUL.

Suggested change
msg["From"] = cfg.email_address or cfg.username
msg["From"] = _validate_header(cfg.email_address or cfg.username, "from")

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

# bracket; for a normal "host:port" just drop the trailing port.
if host.startswith("["):
end = host.find("]")
return host[: end + 1] if end != -1 else host

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Malformed bracketed Host values are accepted after port stripping

_strip_port("[::1]:6969:extra") returns "[::1]" because it stops at the first ] and ignores the trailing suffix. That malformed Host then passes _SAFE_HOST_RE, so CSP frame-src may be generated from a syntactically invalid Host header. Require no trailing characters after the closing bracket, or validate the port suffix, before returning the bracketed host.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/mail_client.py 340 From is assigned from account configuration without _validate_header, leaving the same CRLF/NUL header-injection class open for stored account values.
tinyagentos/middleware/security_headers.py 41 Bracketed Host values with trailing garbage after ] are accepted after port stripping, allowing malformed Host headers to pass _SAFE_HOST_RE and enter CSP frame-src.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
tinyagentos/mail_client.py 375 server.sendmail(cfg.email_address or cfg.username, ...) still uses unvalidated account configuration as the SMTP envelope sender; a CR/LF/NUL there could break out of the SMTP MAIL FROM command even if the MIME From header is fixed.
Files Reviewed (4 files)
  • tests/test_mail_security.py - 0 issues
  • tinyagentos/mail_client.py - 2 issues
  • tinyagentos/middleware/security_headers.py - 1 issue
  • tinyagentos/routes/mail.py - 0 issues

Reviewed by nex-n2-pro:free · 1,066,858 tokens

@jaylfc jaylfc merged commit e3a0bde into dev Jun 16, 2026
9 checks passed
@jaylfc jaylfc deleted the fix/mail-security-hardening branch June 16, 2026 13:38
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant