From the post-batch security review (defense-in-depth follow-up to #203).
Finding
SmtpSender.inlinePart builds the MIME header as contentID = "<${attachment.contentId}>", and GraphSender puts contentId straight into the Graph sendMail JSON — neither strips CR/LF or other ISO control characters.
Not currently exploitable:contentId is always an app-generated img-<uuid>@libremail (ComposeViewModel.onImagePicked), so it can't contain CR/LF today. This is insurance against a future change that lets a user- or external-value flow into contentId, at which point the SMTP path would be a MIME header-injection vector.
Fix
New shared sanitizeContentId(raw: String?) strips ISO control chars (incl. CR/LF), applied at both sinks — the point where the value actually becomes dangerous, so it covers any future origin of contentId:
SmtpSender.inlinePart — before the Content-ID header value.
GraphSender.buildSendMailPayload — before the JSON contentId field.
Sink-side (rather than mint-side) placement mirrors sanitizeAttachmentName from #203 and is the robust choke point. No behavior change for the app-generated ids in use today (they contain no control chars).
Tests
SmtpSenderTest — sends an inline image whose contentId is logo@libremail\r\nX-Injected: evil and asserts, via a real GreenMail SMTP round-trip, that no X-Injected header appears on any MIME part (recursive walk) and the emitted Content-ID stays on a single line.
GraphSenderTest — asserts the crafted contentId is stripped to logo@libremailevil (no CR/LF) in the built payload.
Full local preflight green (JDK 21, --max-workers=8, one at a time): assembleDebug, testDebugUnitTest (SmtpSenderTest 7/7, GraphSenderTest 8/8), lintDebug, ktlintCheck+detekt, compileDebugAndroidTestKotlin. No Room/schema change.
From the post-batch security review (defense-in-depth follow-up to #203).
## Finding
`SmtpSender.inlinePart` builds the MIME header as `contentID = "<${attachment.contentId}>"`, and `GraphSender` puts `contentId` straight into the Graph `sendMail` JSON — neither strips CR/LF or other ISO control characters.
**Not currently exploitable:** `contentId` is always an app-generated `img-<uuid>@libremail` (`ComposeViewModel.onImagePicked`), so it can't contain CR/LF today. This is insurance against a future change that lets a user- or external-value flow into `contentId`, at which point the SMTP path would be a MIME header-injection vector.
## Fix
New shared `sanitizeContentId(raw: String?)` strips ISO control chars (incl. CR/LF), applied at **both sinks** — the point where the value actually becomes dangerous, so it covers any future origin of `contentId`:
- `SmtpSender.inlinePart` — before the `Content-ID` header value.
- `GraphSender.buildSendMailPayload` — before the JSON `contentId` field.
Sink-side (rather than mint-side) placement mirrors `sanitizeAttachmentName` from #203 and is the robust choke point. No behavior change for the app-generated ids in use today (they contain no control chars).
## Tests
- `SmtpSenderTest` — sends an inline image whose `contentId` is `logo@libremail\r\nX-Injected: evil` and asserts, via a real GreenMail SMTP round-trip, that no `X-Injected` header appears on any MIME part (recursive walk) and the emitted `Content-ID` stays on a single line.
- `GraphSenderTest` — asserts the crafted `contentId` is stripped to `logo@libremailevil` (no CR/LF) in the built payload.
Full local preflight green (JDK 21, `--max-workers=8`, one at a time): `assembleDebug`, `testDebugUnitTest` (SmtpSenderTest 7/7, GraphSenderTest 8/8), `lintDebug`, `ktlintCheck`+`detekt`, `compileDebugAndroidTestKotlin`. No Room/schema change.
Closes #204
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
From the post-batch security review (defense-in-depth follow-up to #203).
Finding
SmtpSender.inlinePartbuilds the MIME header ascontentID = "<${attachment.contentId}>", andGraphSenderputscontentIdstraight into the GraphsendMailJSON — neither strips CR/LF or other ISO control characters.Not currently exploitable:
contentIdis always an app-generatedimg-<uuid>@libremail(ComposeViewModel.onImagePicked), so it can't contain CR/LF today. This is insurance against a future change that lets a user- or external-value flow intocontentId, at which point the SMTP path would be a MIME header-injection vector.Fix
New shared
sanitizeContentId(raw: String?)strips ISO control chars (incl. CR/LF), applied at both sinks — the point where the value actually becomes dangerous, so it covers any future origin ofcontentId:SmtpSender.inlinePart— before theContent-IDheader value.GraphSender.buildSendMailPayload— before the JSONcontentIdfield.Sink-side (rather than mint-side) placement mirrors
sanitizeAttachmentNamefrom #203 and is the robust choke point. No behavior change for the app-generated ids in use today (they contain no control chars).Tests
SmtpSenderTest— sends an inline image whosecontentIdislogo@libremail\r\nX-Injected: eviland asserts, via a real GreenMail SMTP round-trip, that noX-Injectedheader appears on any MIME part (recursive walk) and the emittedContent-IDstays on a single line.GraphSenderTest— asserts the craftedcontentIdis stripped tologo@libremailevil(no CR/LF) in the built payload.Full local preflight green (JDK 21,
--max-workers=8, one at a time):assembleDebug,testDebugUnitTest(SmtpSenderTest 7/7, GraphSenderTest 8/8),lintDebug,ktlintCheck+detekt,compileDebugAndroidTestKotlin. No Room/schema change.Closes #204
🤖 Generated with Claude Code