diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php index 124f3c974..00adaa387 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php @@ -9,6 +9,7 @@ use MailSo\Imap\Enumerations\FetchType; trait SMime { private $SMIME = null; + public function SMIME() : OpenSSL { if (!$this->SMIME) { @@ -84,7 +85,7 @@ trait SMime { $sFolderName = $this->GetActionParam('folder', ''); $iUid = (int) $this->GetActionParam('uid', 0); - $sPartId = $this->GetActionParam('partId', ''); + $sPartId = $this->smimePartIdParam('partId'); $sCertificate = $this->GetActionParam('certificate', ''); $sPrivateKey = $this->GetActionParam('privateKey', ''); $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', '')); @@ -137,7 +138,7 @@ trait SMime public function DoSMimeVerifyMessage() : array { $sBody = $this->GetActionParam('bodyPart', ''); - $sPartId = $this->GetActionParam('partId', ''); + $sPartId = $this->smimePartIdParam('partId'); $bDetached = !empty($this->GetActionParam('detached', 0)); if (!$sBody && $sPartId) { $iUid = (int) $this->GetActionParam('uid', 0); @@ -152,7 +153,7 @@ trait SMime // Import the certificates automatically $sBody = $this->GetActionParam('sigPart', ''); - $sPartId = $this->GetActionParam('sigPartId', '') ?: $sPartId; + $sPartId = $this->smimePartIdParam('sigPartId') ?: $sPartId; if (!$sBody && $sPartId && $oImapClient) { $sBody = $oImapClient->Fetch( [FetchType::BODY_PEEK.'['.$sPartId.']'], @@ -189,7 +190,7 @@ trait SMime /* $sBody = $this->GetActionParam('sigPart', ''); if (!$sBody) { - $sPartId = $this->GetActionParam('sigPartId', '') ?: $this->GetActionParam('partId', ''); + $sPartId = $this->smimePartIdParam('sigPartId') ?: $this->smimePartIdParam('partId'); $this->initMailClientConnection(); $oImapClient = $this->ImapClient(); $oImapClient->FolderExamine($this->GetActionParam('folder', '')); @@ -212,4 +213,18 @@ trait SMime return $this->DefaultResponse($certificates); */ } + + /** + * A part id from the client goes into an IMAP FETCH section, BODY.PEEK[...], + * as is. Accept only a section number (1, 1.2.3) or TEXT, so the value cannot + * close the bracket or the line and add commands of its own. + */ + private function smimePartIdParam(string $sKey): string + { + $sPartId = \trim((string) $this->GetActionParam($sKey, '')); + if ('' !== $sPartId && !\preg_match('/^(TEXT|[1-9][0-9]*(\.[1-9][0-9]*)*)$/D', $sPartId)) { + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::InvalidInputArgument); + } + return $sPartId; + } } diff --git a/test/smime-partid.php b/test/smime-partid.php new file mode 100644 index 000000000..2cdc80665 --- /dev/null +++ b/test/smime-partid.php @@ -0,0 +1,37 @@ +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";