snappymail/test/smime-partid.php
djmaze 099ee3b97e @ZaphodB fix(@security): S/MIME actions put the client's part id into IMAP FETCH as is
DoSMimeDecryptMessage and DoSMimeVerifyMessage built BODY.PEEK[<partId>]
(and [<partId>.MIME]) from the partId / sigPartId parameters without checking
them, so a value could close the bracket or the line and add IMAP commands of
its own. They run in the user's own IMAP session, so this is not cross-account,
but nothing in that session should be steerable by a request parameter.

Read both through smimePartIdParam(), which accepts only an IMAP section number
(1, 1.2.10) or TEXT and refuses anything else.

test/smime-partid.php checks valid ids pass and injection attempts, zero or
leading-zero sections, lowercase and other section keywords are refused.
2026-10-27 22:50:28 +01:00

37 lines
1.3 KiB
PHP

<?php
// Run with: php test/smime-partid.php
// Enable SnappyMail Api and include index file
$_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true;
require __DIR__ . '/../index.php';
function check(bool $condition, string $message): void
{
if (!$condition) {
throw new RuntimeException($message);
}
}
$actions = (new ReflectionClass(\RainLoop\Actions::class))->newInstanceWithoutConstructor();
$params = new ReflectionProperty(\RainLoop\Actions::class, 'aCurrentActionParams');
$method = new ReflectionMethod(\RainLoop\Actions::class, 'smimePartIdParam');
$read = function (string $value) use ($actions, $params, $method) : ?string {
$params->setValue($actions, ['partId' => $value]);
try {
return $method->invoke($actions, 'partId');
} catch (\RainLoop\Exceptions\ClientException $e) {
return null;
}
};
foreach (['', '1', '2', '1.2', '1.2.10', 'TEXT'] as $ok) {
check($ok === $read($ok), "valid part id '{$ok}' refused");
}
foreach (["1]\r\nA1 LOGOUT\r\n", '1] BODY[HEADER', "1\r\nA1 LOGOUT", '1 2', '0', '1.0', '01', '1.', '.1', 'text', 'HEADER'] as $bad) {
check(null === $read($bad), 'part id '.json_encode($bad).' accepted');
}
// Surrounding whitespace is trimmed away, and the trimmed value is what is used.
check('1' === $read("1\n"), 'trailing newline not trimmed');
echo "smime-partid: ok\n";