Skip to content

Commit e07b966

Browse files
committed
Spoofing detection added Webklex#40
1 parent f5dd368 commit e07b966

File tree

5 files changed

+167
-2
lines changed

5 files changed

+167
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
99
- Filename sanitization is now optional (enabled via default)
1010

1111
### Added
12-
- NaN
12+
- Spoofing detection added #40
1313

1414
### Breaking changes
1515
- NaN
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/*
3+
* File: SpoofingAttemptDetectedException.php
4+
* Category: Exception
5+
* Author: M. Goldenbaum
6+
* Created: 17.01.25 17:25
7+
* Updated: -
8+
*
9+
* Description:
10+
* -
11+
*/
12+
13+
namespace Webklex\PHPIMAP\Exceptions;
14+
15+
use \Exception;
16+
17+
/**
18+
* Class SpoofingAttemptDetectedException
19+
*
20+
* @package Webklex\PHPIMAP\Exceptions
21+
*/
22+
class SpoofingAttemptDetectedException extends Exception {
23+
24+
}

src/Header.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Webklex\PHPIMAP\Exceptions\DecoderNotFoundException;
1919
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
2020
use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
21+
use Webklex\PHPIMAP\Exceptions\SpoofingAttemptDetectedException;
2122

2223
/**
2324
* Class Header
@@ -199,6 +200,7 @@ private function clearBoundaryString(string $str): string {
199200
* Parse the raw headers
200201
*
201202
* @throws InvalidMessageDateException
203+
* @throws SpoofingAttemptDetectedException
202204
*/
203205
protected function parse(): void {
204206
$header = $this->rfc822_parse_headers($this->raw);
@@ -230,6 +232,11 @@ protected function parse(): void {
230232

231233
$this->extractHeaderExtensions();
232234
$this->findPriority();
235+
236+
if($this->config->get('security.detect_spoofing', true)) {
237+
// Detect spoofing
238+
$this->detectSpoofing();
239+
}
233240
}
234241

235242
/**
@@ -413,7 +420,7 @@ private function decodeAddresses($values): array {
413420
* @param object $header
414421
*/
415422
private function extractAddresses(object $header): void {
416-
foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) {
423+
foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender', 'return_path', 'envelope_from', 'envelope_to', 'delivered_to'] as $key) {
417424
if (property_exists($header, $key)) {
418425
$this->set($key, $this->parseAddresses($header->$key));
419426
}
@@ -760,4 +767,25 @@ public function setDecoder(DecoderInterface $decoder): static {
760767
return $this;
761768
}
762769

770+
/**
771+
* Detect spoofing by checking the from, reply_to, return_path, sender and envelope_from headers
772+
* @throws SpoofingAttemptDetectedException
773+
*/
774+
private function detectSpoofing(): void {
775+
$header_keys = ["from", "reply_to", "return_path", "sender", "envelope_from"];
776+
$potential_senders = [];
777+
foreach($header_keys as $key) {
778+
$header = $this->get($key);
779+
foreach ($header->toArray() as $address) {
780+
$potential_senders[] = $address->mailbox . "@" . $address->host;
781+
}
782+
}
783+
if(count($potential_senders) > 1) {
784+
$this->set("spoofed", true);
785+
if($this->config->get('security.detect_spoofing_exception', false)) {
786+
throw new SpoofingAttemptDetectedException("Potential spoofing detected. Message ID: " . $this->get("message_id") . " Senders: " . implode(", ", $potential_senders));
787+
}
788+
}
789+
}
790+
763791
}

tests/issues/Issue40Test.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
/*
3+
* File: Issue410Test.php
4+
* Category: -
5+
* Author: M.Goldenbaum
6+
* Created: 23.06.23 20:41
7+
* Updated: -
8+
*
9+
* Description:
10+
* -
11+
*/
12+
13+
namespace Tests\issues;
14+
15+
use PHPUnit\Framework\TestCase;
16+
use Tests\fixtures\FixtureTestCase;
17+
use Webklex\PHPIMAP\Attachment;
18+
use Webklex\PHPIMAP\ClientManager;
19+
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
20+
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
21+
use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
22+
use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
23+
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
24+
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
25+
use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
26+
use Webklex\PHPIMAP\Exceptions\ResponseException;
27+
use Webklex\PHPIMAP\Exceptions\RuntimeException;
28+
use Webklex\PHPIMAP\Exceptions\SpoofingAttemptDetectedException;
29+
use Webklex\PHPIMAP\Message;
30+
31+
class Issue40Test extends FixtureTestCase {
32+
33+
/**
34+
* @throws RuntimeException
35+
* @throws MessageContentFetchingException
36+
* @throws ResponseException
37+
* @throws ImapBadRequestException
38+
* @throws InvalidMessageDateException
39+
* @throws ConnectionFailedException
40+
* @throws \ReflectionException
41+
* @throws ImapServerErrorException
42+
* @throws AuthFailedException
43+
* @throws MaskNotFoundException
44+
*/
45+
public function testIssueEmail() {
46+
$message = $this->getFixture("issue-40.eml");
47+
48+
self::assertSame("Zly from", (string)$message->subject);
49+
self::assertSame([
50+
'personal' => '',
51+
'mailbox' => 'faked_sender',
52+
'host' => 'sender_domain.pl',
53+
'mail' => 'faked_sender@sender_domain.pl',
54+
'full' => 'faked_sender@sender_domain.pl',
55+
], $message->from->first()->toArray());
56+
self::assertSame([
57+
'personal' => '<real_sender@sender_domain.pl>',
58+
'mailbox' => 'real_sender',
59+
'host' => 'sender_domain.pl',
60+
'mail' => 'real_sender@sender_domain.pl',
61+
'full' => '<real_sender@sender_domain.pl> <real_sender@sender_domain.pl>',
62+
], (array)$message->return_path->first());
63+
self::assertSame(true, $message->spoofed->first());
64+
65+
$config = $message->getConfig();
66+
self::assertSame(false, $config->get("security.detect_spoofing_exception"));
67+
$config->set("security.detect_spoofing_exception", true);
68+
self::assertSame(true, $config->get("security.detect_spoofing_exception"));
69+
70+
$this->expectException(SpoofingAttemptDetectedException::class);
71+
$this->getFixture("issue-40.eml", $config);
72+
}
73+
74+
}

tests/messages/issue-40.eml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Return-Path: <real_sender@sender_domain.pl>
2+
Delivered-To: receipent@receipent_domain.pl
3+
Received: from h2.server.pl
4+
\tby h2.server.pl with LMTP
5+
\tid 4IDTIEUkm18ZSSkA87l24w
6+
\t(envelope-from <real_sender@sender_domain.pl>)
7+
\tfor <receipent@receipent_domain.pl>; Thu, 29 Oct 2020 21:21:25 +0100
8+
Return-path: <real_sender@sender_domain.pl>
9+
Envelope-to: receipent@receipent_domain.pl
10+
Delivery-date: Thu, 29 Oct 2020 21:21:25 +0100
11+
Received: from sender_domain.pl ([server ip])
12+
\tby h2.server.pl with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
13+
\t(Exim 4.94)
14+
\t(envelope-from <real_sender@sender_domain.pl>)
15+
\tid 1kYEQG-00BPgD-S0
16+
\tfor receipent@receipent_domain.pl; Thu, 29 Oct 2020 21:21:25 +0100
17+
Received: by sender_domain.pl (Postfix, from userid 1000)
18+
\tid 57DADAB; Thu, 29 Oct 2020 21:21:23 +0100 (CET)
19+
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=sender_domain.pl; s=default;
20+
\tt=1604002883; bh=CsZufJouWdjY/W12No6MSSMwbp0VaS8EOMGg9WptEaI=;
21+
\th=From:To:Subject:Date;
22+
\tb=v0NAncnNT/w+gInANxAkMt20ktM4LZquuwlokUmLpPyO3++8dy112olu63Dkn9L2E
23+
\t GwfHGqW+8f7g494UK6asUKqTx8fHxlEJbHqAiEV5QrlynSeZDFXsKvGDW8XNMFBKop
24+
\t sAjvp8NTUiNcA4MTbFaZ7RX15A/9d9QVEynU8MaNP2ZYKnq9J/JXgUjjMnx+FiULqf
25+
\t xJN/5rjwHRx7f6JQoXXUxuck6Zh4tSDiLLnDFasrSxed6sTNfnZMAggCyb1++estNk
26+
\t q6HNBwp85Az3ELo10RbBF/WM2FhxxFz1khncRtCyLXLUZ2lzhjan765KXpeYg7FUa9
27+
\t zItPWVTaTzTEg==
28+
From: faked_sender@sender_domain.pl
29+
To: receipent@receipent_domain.pl
30+
Subject: Zly from
31+
Message-Id: <20201029202123.57DADAB@sender_domain.pl>
32+
Date: Thu, 29 Oct 2020 21:21:01 +0100 (CET)
33+
Forward-Confirmed-ReverseDNS: Reverse and forward lookup success on server ip, -10 Spam score
34+
SPFCheck: Server passes SPF test, -30 Spam score
35+
X-DKIM: signer='sender_domain.pl' status='pass' reason=''
36+
DKIMCheck: Server passes DKIM test, -20 Spam score
37+
X-Spam-Score: -0.2 (/)
38+
39+
Test message

0 commit comments

Comments
 (0)