Issue 3231 fix unsigned html alternative - #3244
Open
DenBond7 wants to merge 7 commits into
Open
Conversation
…HTML - Ensure selectAlternativeContent renders signed text/plain alternative when text/html is unsigned. - Prevent displaying attacker-controlled unsigned HTML under a valid Signed PGP badge. - Update unit and UI regression tests for signed-plaintext-unsigned-html-alternative fixtures.
DenBond7
marked this pull request as ready for review
July 31, 2026 13:53
Collaborator
Author
|
@sosnovsky This one is ready |
sosnovsky
reviewed
Aug 3, 2026
sosnovsky
reviewed
Aug 4, 2026
…rnative selection
sosnovsky
reviewed
Aug 6, 2026
…ejected MIME alternatives`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR resolves a security vulnerability where an unsigned HTML alternative body could be displayed while simultaneously presenting a Signed PGP badge when an email contains a cleartext/inline signed
text/plainalternative alongside an unsignedtext/htmlalternative.🔒 Problem & Context
Previously, when parsing
multipart/alternativeemails:SignedMsgBlock) inplainBlocks, causing it to fall through and select the firstotherBlocksentry (unsignedtext/html) for rendering.multipart/alternativestructures without selection caching resulted in exponential recomputation, allowing deeply nested alternatives to stall message processing.🛠 What was done
Prioritize Signed Alternatives (
PgpMsg.kt):Updated
AlternativeContentResolverto determine whetherplainBlockscontains PGP-signed content (SIGNED_BLOCK_TYPESorisOpenPGPMimeSigned). IfplainBlocksis signed and the first candidate inotherBlocksis unsigned, the resolver selects the signed plaintext version for rendering.Displayed Candidate Resolution (
PgpMsg.kt):Added
hasSignedDisplayedContent, which first resolves nested alternatives throughgetDisplayedBlocks()and then checks only the blocks that will actually be displayed. At the current alternative level, onlyotherBlocks.take(1)is considered because it represents the rendering candidate. This prevents hidden or later signed parts inside nested alternatives from incorrectly influencing the outer alternative selection.Exponential Recursion Mitigation (
AlternativeContentResolver):Encapsulated alternative resolution in
AlternativeContentResolver, backed by anIdentityHashMapselection cache. This ensures eachAlternativeContentMsgBlockselection is evaluated at most once and avoids exponential recomputation for deeply nested MIME structures.Selection-Aware Inline Images (
PgpMsg.kt):Updated inline-image collection to follow the selected MIME alternative. Unsigned inline images from a rejected HTML alternative are no longer included when signed plaintext is selected, while CID images belonging to the displayed HTML alternative remain supported.
Regression Tests (
ProcessMimeMessageTest.kt,PgpMsgInlineImageAlternativeTest.kt, andMessageDetailsFlowTest.kt):multipart/alternativeregression test where the nested displayed HTML is unsigned but a later hidden nested part is signed. The test verifies that the valid signed outer plaintext is still selected and that hidden signed content cannot influence the selection.multipart/alternativemessage with three alternatives: signed plaintext, unsigned first HTML, and a signed third alternative. This ensures that only the actual rendering candidate affects selection.testProcessesDeeplyNestedAlternativesWithoutRepeatedSelection) with 25 levels of nestedmultipart/alternativestructures to guard against a recurrence of exponential processing slowdown.Implementation details
Before
filterBlocksViaTreeran inside:It analyzed all MIME alternatives when determining both encryption and signature status.
For a message containing:
text/plain;text/html;the function detected the signature in the plaintext alternative, while the renderer selected the HTML alternative.
As a result, unsigned HTML content was displayed with a Signed badge.
After
The processing is now split into three sequential phases:
AlternativeContentResolver.getDisplayedBlocks()resolves the alternative branch selected for rendering. Only the resolved displayed blocks are used to calculate the signature status.The remaining loop:
now only separates content blocks from result blocks for formatting.
Why
Signature status must describe the content that is actually displayed.
Moving signature analysis outside the original loop separates:
This prevents a signature from a hidden MIME alternative from being applied to the alternative selected for rendering.
close #3231
Tests (delete all except exactly one):
To be filled by reviewers
I have reviewed that this PR... (tick whichever items you personally focused on during this review):