diff --git a/composer.json b/composer.json index 2ffdf86609..284ae24e52 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "bytestream/horde-util": "^2.7.0", "cerdic/css-tidy": "v2.1.0", "ezyang/htmlpurifier": "4.17.0", + "glenscott/url-normalizer": "^1.4", "gravatarphp/gravatar": "dev-master#6b9f6a45477ce48285738d9d0c3f0dbf97abe263", "hamza221/html2text": "^1.0", "jeremykendall/php-domain-parser": "^6.3", diff --git a/composer.lock b/composer.lock index 0d04292a41..721da03a3b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "198b503625a39a6c6a9108a94acdea9e", + "content-hash": "0cc234da73403beae6482cd827b5dc06", "packages": [ { "name": "amphp/amp", @@ -1706,6 +1706,47 @@ }, "time": "2023-11-17T15:01:25+00:00" }, + { + "name": "glenscott/url-normalizer", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/glenscott/url-normalizer.git", + "reference": "b8e79d3360a1bd7182398c9956bd74d219ad1b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/glenscott/url-normalizer/zipball/b8e79d3360a1bd7182398c9956bd74d219ad1b3c", + "reference": "b8e79d3360a1bd7182398c9956bd74d219ad1b3c", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "URL\\": "src/URL" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Glen Scott", + "email": "glen@glenscott.co.uk" + } + ], + "description": "Syntax based normalization of URL's", + "support": { + "issues": "https://github.com/glenscott/url-normalizer/issues", + "source": "https://github.com/glenscott/url-normalizer/tree/master" + }, + "time": "2015-06-11T16:06:02+00:00" + }, { "name": "gravatarphp/gravatar", "version": "dev-master", diff --git a/lib/Service/PhishingDetection/LinkCheck.php b/lib/Service/PhishingDetection/LinkCheck.php new file mode 100644 index 0000000000..cc510f14a5 --- /dev/null +++ b/lib/Service/PhishingDetection/LinkCheck.php @@ -0,0 +1,80 @@ +l10n = $l10n; + } + // checks if link text is meant to look like a link + private function textLooksLikeALink(string $text): bool { + // based on https://gist.github.com/gruber/8891611 + $pattern = '/(?i)\b((?:https?:(?:\/{1,3}|[a-z0-9%])|[a-z0-9.\-]+[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)\/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’])|(?:(?childNodes as $child) { + if ($child->nodeType === XML_TEXT_NODE) { + $innerText .= $child->nodeValue; + } elseif ($child->nodeType === XML_ELEMENT_NODE) { + $innerText .= $this->getInnerText($child); + } + } + return $innerText; + } + + public function run(string $htmlMessage) : PhishingDetectionResult { + + $results = []; + $zippedArray = []; + + $dom = new \DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML($htmlMessage); + libxml_use_internal_errors(); + $anchors = $dom->getElementsByTagName('a'); + foreach ($anchors as $anchor) { + $href = $anchor->getAttribute('href'); + $linkText = $this->getInnerText($anchor); + $zippedArray[] = [ + 'href' => $href, + 'linkText' => $linkText + ]; + } + foreach ($zippedArray as $zipped) { + $un = new Normalizer($zipped['href']); + $url = $un->normalize(); + if($this->textLooksLikeALink($zipped['linkText'])) { + if(parse_url($url, PHP_URL_HOST) !== parse_url($zipped['linkText'], PHP_URL_HOST)) { + $results[] = [ + 'href' => $url, + 'linkText' => $zipped['linkText'], + ]; + } + } + } + if(count($results) > 0) { + return new PhishingDetectionResult(PhishingDetectionResult::LINK_CHECK, true, $this->l10n->t('Some addresses in this message are not matching the link text'), $results); + } + return new PhishingDetectionResult(PhishingDetectionResult::LINK_CHECK, false); + + } + +} diff --git a/lib/Service/PhishingDetection/PhishingDetectionService.php b/lib/Service/PhishingDetection/PhishingDetectionService.php index 7c0896daa1..9b0852a664 100644 --- a/lib/Service/PhishingDetection/PhishingDetectionService.php +++ b/lib/Service/PhishingDetection/PhishingDetectionService.php @@ -14,11 +14,12 @@ use OCA\Mail\PhishingDetectionList; class PhishingDetectionService { - public function __construct(private ContactCheck $contactCheck, private CustomEmailCheck $customEmailCheck, private DateCheck $dateCheck, private ReplyToCheck $replyToCheck) { + public function __construct(private ContactCheck $contactCheck, private CustomEmailCheck $customEmailCheck, private DateCheck $dateCheck, private ReplyToCheck $replyToCheck, private LinkCheck $linkCheck) { $this->contactCheck = $contactCheck; $this->customEmailCheck = $customEmailCheck; $this->dateCheck = $dateCheck; $this->replyToCheck = $replyToCheck; + $this->linkCheck = $linkCheck; } @@ -38,6 +39,9 @@ public function checkHeadersForPhishing(Horde_Mime_Headers $headers, bool $hasHt $list->addCheck($this->contactCheck->run($fromFN, $fromEmail)); $list->addCheck($this->dateCheck->run($date)); $list->addCheck($this->customEmailCheck->run($fromEmail, $customEmail)); + if ($hasHtmlMessage) { + $list->addCheck($this->linkCheck->run($htmlMessage)); + } return $list->jsonSerialize(); } } diff --git a/src/components/PhishingWarning.vue b/src/components/PhishingWarning.vue index 24a1574610..ffaec930ce 100644 --- a/src/components/PhishingWarning.vue +++ b/src/components/PhishingWarning.vue @@ -11,16 +11,28 @@