Headline
CVE-2023-3080: Changeset 2924014 for wp-mail-catcher – WordPress Plugin Repository
The WP Mail Catcher plugin for WordPress is vulnerable to Stored Cross-Site Scripting via an email subject in versions up to, and including, 2.1.2 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
wp-mail-catcher/trunk/WpMailCatcher.php
r2922550
r2924014
7
7
Description: Logging your mail will stop you from ever losing your emails again! This fast, lightweight plugin (under 140kb in size!) is also useful for debugging or backing up your messages.
8
8
Author: James Ward
9
 
Version: 2.1.2
 
9
Version: 2.1.3
10
10
Author URI: https://jamesward.io
11
11
Donate link: https://paypal.me/jamesmward
wp-mail-catcher/trunk/readme.txt
r2922550
r2924014
3
3
Tags: mail logging, email log, email logger, logging, email logging, mail, crm
4
4
Requires at least: 4.7
5
 
Tested up to: 6.2.2
 
5
Tested up to: 6.2.3
6
6
Requires PHP: 7.4
7
 
Stable tag: 2.1.2
 
7
Stable tag: 2.1.3
8
8
License: GNU General Public License v3.0
9
9
License URI: https://raw.githubusercontent.com/JWardee/wp-mail-catcher/master/LICENSE
…
…
 
95
95
\== Changelog ==
96
96
 
97
\= 2.1.3 =
 
98
 
99
\- Fix: Improved HTML email detection
 
100
\- Fix: Improved XSS filtering
 
101
97
102
\= 2.1.2 =
98
103
wp-mail-catcher/trunk/src/GeneralHelper.php
r2922541
r2924014
28
28
    public static $reviewLink;
29
29
    public static $actionNameSpace;
30
 
    public static $htmlEmailHeader = 'content-type: text/html;';
 
30
    public static $htmlEmailHeader = 'content-type: text/html';
31
31
32
32
    public static function setSettings()
…
…
 
133
133
    }
134
134
135
 
    public static function sanitiseForQuery($value)
 
135
    public static function sanitiseForDbQuery($value)
136
136
    {
137
137
        switch (gettype($value)) {
…
…
 
149
149
    }
150
150
151
 
    public static function sanitiseHtmlspecialchars($input): string
152
 
    {
153
 
        return htmlspecialchars(
154
 
            $input,
155
 
            ENT\_QUOTES | ENT\_SUBSTITUTE | ENT\_HTML401,
156
 
            null,
157
 
            false
158
 
        );
 
151
    private static function getAllowedTags()
 
152
    {
 
153
        $tags = wp\_kses\_allowed\_html('post');
 
154
        $tags\['style'\] = \[\];
 
155
        return $tags;
 
156
    }
 
157
 
158
    public static function filterHtml($value)
 
159
    {
 
160
        return wp\_kses($value, self::getAllowedTags());
159
161
    }
160
162
…
…
 
167
169
        global $wpdb;
168
170
169
 
        $urls = self::sanitiseForQuery($urls);
 
171
        $urls = self::sanitiseForDbQuery($urls);
170
172
171
173
        $sql = "SELECT DISTINCT post\_id
wp-mail-catcher/trunk/src/Loggers/BuddyPress.php
r2920307
r2924014
45
45
        return \[
46
46
            'time' => time(),
47
 
            'email\_to' => GeneralHelper::arrayToString($tos),
48
 
            'subject' => $bpMail->get\_subject(),
49
 
            'message' => $this->sanitiseInput($bpMail->get\_content()),
 
47
            'email\_to' => GeneralHelper::filterHtml(GeneralHelper::arrayToString($tos)),
 
48
            'subject' => GeneralHelper::filterHtml($bpMail->get\_subject()),
 
49
            'message' => GeneralHelper::filterHtml($bpMail->get\_content()),
50
50
            'backtrace\_segment' => json\_encode($this->getBacktrace('bp\_send\_email')),
51
51
            'status' => 1,
wp-mail-catcher/trunk/src/Loggers/LogHelper.php
r2922541
r2924014
143
143
    }
144
144
145
 
    protected function sanitiseInput($input): string
146
 
    {
147
 
        return htmlspecialchars(
148
 
            $input,
149
 
            ENT\_QUOTES | ENT\_SUBSTITUTE | ENT\_HTML401,
150
 
            null,
151
 
            false
152
 
        );
153
 
    }
154
 
155
 
    protected function sanitiseAndRemoveScripts($input): string
156
 
    {
157
 
        return preg\_replace(
158
 
            '#<script(.\*?)>(.\*?)</script>#is',
159
 
            '',
160
 
            GeneralHelper::sanitiseHtmlspecialchars($input)
161
 
        );
162
 
    }
163
 
164
145
    /\*\*
165
146
     \* Get the details of the method that originally triggered wp\_mail
wp-mail-catcher/trunk/src/Loggers/WpMail.php
r2922541
r2924014
43
43
        return \[
44
44
            'time' => time(),
45
 
            'email\_to' => $this->sanitiseInput(GeneralHelper::arrayToString($args\['to'\])),
46
 
            'subject' => $this->sanitiseInput($args\['subject'\]),
47
 
            'message' => $this->sanitiseAndRemoveScripts($args\['message'\]),
 
45
            'email\_to' => GeneralHelper::filterHtml(GeneralHelper::arrayToString($args\['to'\])),
 
46
            'subject' => GeneralHelper::filterHtml($args\['subject'\]),
 
47
            'message' => GeneralHelper::filterHtml($args\['message'\]),
48
48
            'backtrace\_segment' => json\_encode($this->getBacktrace()),
49
49
            'status' => 1,
wp-mail-catcher/trunk/src/MailAdminTable.php
r2922541
r2924014
30
30
31
31
        return self::$instance;
 
32
    }
 
33
 
34
    private function runHtmlSpecialChars($value)
 
35
    {
 
36
        $value = GeneralHelper::filterHtml($value);
 
37
 
38
        return htmlspecialchars(
 
39
            $value,
 
40
            ENT\_QUOTES | ENT\_SUBSTITUTE | ENT\_HTML401,
 
41
            null,
 
42
            false
 
43
        );
32
44
    }
33
45
…
…
 
59
71
60
72
            $subjectDecoded = base64\_decode($subjectEncoded);
61
 
            $subjectDecoded = GeneralHelper::sanitiseHtmlspecialchars($subjectDecoded);
 
73
            $subjectDecoded = $this->runHtmlSpecialChars($subjectDecoded);
 
74
62
75
            return '<span class="asci-help" data-hover-message="' . \_\_("This subject was base64 decoded") . '">
63
76
                        <a href="' . $this->asciSubjectHelpLink . '" target="\_blank">(?)</a>
…
…
 
75
88
            $subjectDecoded = quoted\_printable\_decode($subjectEncoded);
76
89
            $subjectDecoded = base64\_decode($subjectEncoded);
77
 
            $subjectDecoded = GeneralHelper::sanitiseHtmlspecialchars($subjectDecoded);
 
90
            $subjectDecoded = $this->runHtmlSpecialChars($subjectDecoded);
 
91
78
92
            return '<span class="asci-help" data-hover-message="' . \_\_("This subject was quoted printable decoded") . '">
79
93
                        <a href="' . $this->asciSubjectHelpLink . '" target="\_blank">(?)</a>
…
…
 
82
96
        }
83
97
84
 
        return GeneralHelper::sanitiseHtmlspecialchars($subject);
 
98
        return $this->runHtmlSpecialChars($subject);
85
99
    }
86
100
…
…
 
126
140
        \];
127
141
128
 
        return sprintf('%1$s %2$s', GeneralHelper::sanitiseHtmlspecialchars($item\['email\_to'\]), $this->row\_actions($actions));
 
142
        $emailTo = $this->runHtmlSpecialChars($item\['email\_to'\]);
 
143
 
144
        return sprintf('%1$s %2$s', $emailTo, $this->row\_actions($actions));
129
145
    }
130
146
wp-mail-catcher/trunk/src/Models/Logs.php
r2922546
r2924014
85
85
         \* Sanitise each value in the array
86
86
         \*/
87
 
        array\_walk\_recursive($args, 'WpMailCatcher\\GeneralHelper::sanitiseForQuery');
 
87
        array\_walk\_recursive($args, 'WpMailCatcher\\GeneralHelper::sanitiseForDbQuery');
88
88
89
89
        $sql = "SELECT " . implode(',', $columnsToSelect) . "
…
…
 
181
181
            } elseif (isset($result\['additional\_headers'\])) {
182
182
                $result\['is\_html'\] = GeneralHelper::doesArrayContainSubString(
183
 
                    $result\['additional\_headers'\],
184
 
                    GeneralHelper::$htmlEmailHeader
 
183
                    str\_replace(' ', '', $result\['additional\_headers'\]),
 
184
                    str\_replace(' ', '', GeneralHelper::$htmlEmailHeader)
185
185
                );
186
 
            }
187
 
188
 
            if (isset($result\['message'\])) {
189
 
                $result\['message'\] = stripslashes(htmlspecialchars\_decode($result\['message'\]));
190
 
            }
191
 
192
 
            if (isset($result\['subject'\])) {
193
 
                $result\['subject'\] = stripslashes(htmlspecialchars\_decode($result\['subject'\]));
194
 
            }
195
 
196
 
            if (isset($result\['email\_to'\])) {
197
 
                $result\['email\_to'\] = stripslashes(htmlspecialchars\_decode($result\['email\_to'\]));
198
186
            }
199
187
…
…
 
239
227
240
228
        $ids = GeneralHelper::arrayToString($ids);
241
 
        $ids = GeneralHelper::sanitiseForQuery($ids);
 
229
        $ids = GeneralHelper::sanitiseForDbQuery($ids);
242
230
243
231
        $wpdb->query("DELETE FROM " . $wpdb->prefix . GeneralHelper::$tableName . "
wp-mail-catcher/trunk/src/Models/Mail.php
r2920307
r2924014
21
21
22
22
            add\_filter('wp\_mail\_content\_type', $updateContentType, self::$contentTypeFilterPriority);
 
23
 
24
            if (isset($log\['message'\])) {
 
25
                $log\['message'\] = GeneralHelper::filterHtml($log\['message'\]);
 
26
            }
 
27
 
28
            if (isset($log\['subject'\])) {
 
29
                $log\['subject'\] = GeneralHelper::filterHtml($log\['subject'\]);
 
30
            }
23
31
24
32
            wp\_mail(
…
…
 
54
62
                return in\_array($key, GeneralHelper::$csvExportLegalColumns);
55
63
            }, ARRAY\_FILTER\_USE\_KEY);
 
64
 
65
            if (isset($log\['message'\])) {
 
66
                $log\['message'\] = GeneralHelper::filterHtml($log\['message'\]);
 
67
            }
 
68
 
69
            if (isset($log\['subject'\])) {
 
70
                $log\['subject'\] = GeneralHelper::filterHtml($log\['subject'\]);
 
71
            }
56
72
57
73
            if (isset($log\['attachments'\]) && !empty($log\['attachments'\]) && is\_array($log\['attachments'\])) {
wp-mail-catcher/trunk/src/Views/HtmlMessage.php
r2920307
r2924014
1
1
<?php
2
2
3
 
echo $log\['message'\] ?? '';
 
3
use WpMailCatcher\\GeneralHelper;
 
4
 
5
echo GeneralHelper::filterHtml($log\['message'\] ?? '');