Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/mail/UserMailer.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Classes used to send e-mails
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author <[email protected]>
22
 * @author <[email protected]>
23
 * @author Tim Starling
24
 * @author Luke Welling [email protected]
25
 */
26
27
/**
28
 * Collection of static functions for sending mail
29
 */
30
class UserMailer {
31
	private static $mErrorString;
32
33
	/**
34
	 * Send mail using a PEAR mailer
35
	 *
36
	 * @param Mail_smtp $mailer
37
	 * @param string $dest
38
	 * @param string $headers
39
	 * @param string $body
40
	 *
41
	 * @return Status
42
	 */
43
	protected static function sendWithPear( $mailer, $dest, $headers, $body ) {
44
		$mailResult = $mailer->send( $dest, $headers, $body );
45
46
		// Based on the result return an error string,
47 View Code Duplication
		if ( PEAR::isError( $mailResult ) ) {
48
			wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" );
49
			return Status::newFatal( 'pear-mail-error', $mailResult->getMessage() );
50
		} else {
51
			return Status::newGood();
52
		}
53
	}
54
55
	/**
56
	 * Creates a single string from an associative array
57
	 *
58
	 * @param array $headers Associative Array: keys are header field names,
59
	 *                 values are ... values.
60
	 * @param string $endl The end of line character.  Defaults to "\n"
61
	 *
62
	 * Note RFC2822 says newlines must be CRLF (\r\n)
63
	 * but php mail naively "corrects" it and requires \n for the "correction" to work
64
	 *
65
	 * @return string
66
	 */
67
	static function arrayToHeaderString( $headers, $endl = PHP_EOL ) {
68
		$strings = [];
69
		foreach ( $headers as $name => $value ) {
70
			// Prevent header injection by stripping newlines from value
71
			$value = self::sanitizeHeaderValue( $value );
72
			$strings[] = "$name: $value";
73
		}
74
		return implode( $endl, $strings );
75
	}
76
77
	/**
78
	 * Create a value suitable for the MessageId Header
79
	 *
80
	 * @return string
81
	 */
82
	static function makeMsgId() {
83
		global $wgSMTP, $wgServer;
84
85
		$msgid = uniqid( wfWikiID() . ".", true ); /* true required for cygwin */
86
		if ( is_array( $wgSMTP ) && isset( $wgSMTP['IDHost'] ) && $wgSMTP['IDHost'] ) {
87
			$domain = $wgSMTP['IDHost'];
88
		} else {
89
			$url = wfParseUrl( $wgServer );
90
			$domain = $url['host'];
91
		}
92
		return "<$msgid@$domain>";
93
	}
94
95
	/**
96
	 * This function will perform a direct (authenticated) login to
97
	 * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an
98
	 * array of parameters. It requires PEAR:Mail to do that.
99
	 * Otherwise it just uses the standard PHP 'mail' function.
100
	 *
101
	 * @param MailAddress|MailAddress[] $to Recipient's email (or an array of them)
102
	 * @param MailAddress $from Sender's email
103
	 * @param string $subject Email's subject.
104
	 * @param string $body Email's text or Array of two strings to be the text and html bodies
105
	 * @param array $options:
0 ignored issues
show
There is no parameter named $options:. Did you maybe mean $options?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
106
	 * 		'replyTo' MailAddress
107
	 * 		'contentType' string default 'text/plain; charset=UTF-8'
108
	 * 		'headers' array Extra headers to set
109
	 *
110
	 * @throws MWException
111
	 * @throws Exception
112
	 * @return Status
113
	 */
114
	public static function send( $to, $from, $subject, $body, $options = [] ) {
115
		global $wgAllowHTMLEmail;
116
117
		if ( !isset( $options['contentType'] ) ) {
118
			$options['contentType'] = 'text/plain; charset=UTF-8';
119
		}
120
121
		if ( !is_array( $to ) ) {
122
			$to = [ $to ];
123
		}
124
125
		// mail body must have some content
126
		$minBodyLen = 10;
127
		// arbitrary but longer than Array or Object to detect casting error
128
129
		// body must either be a string or an array with text and body
130
		if (
131
			!(
132
				!is_array( $body ) &&
133
				strlen( $body ) >= $minBodyLen
134
			)
135
			&&
136
			!(
137
				is_array( $body ) &&
138
				isset( $body['text'] ) &&
139
				isset( $body['html'] ) &&
140
				strlen( $body['text'] ) >= $minBodyLen &&
141
				strlen( $body['html'] ) >= $minBodyLen
142
			)
143
		) {
144
			// if it is neither we have a problem
145
			return Status::newFatal( 'user-mail-no-body' );
146
		}
147
148
		if ( !$wgAllowHTMLEmail && is_array( $body ) ) {
149
			// HTML not wanted.  Dump it.
150
			$body = $body['text'];
151
		}
152
153
		wfDebug( __METHOD__ . ': sending mail to ' . implode( ', ', $to ) . "\n" );
154
155
		// Make sure we have at least one address
156
		$has_address = false;
157
		foreach ( $to as $u ) {
158
			if ( $u->address ) {
159
				$has_address = true;
160
				break;
161
			}
162
		}
163
		if ( !$has_address ) {
164
			return Status::newFatal( 'user-mail-no-addy' );
165
		}
166
167
		// give a chance to UserMailerTransformContents subscribers who need to deal with each
168
		// target differently to split up the address list
169
		if ( count( $to ) > 1 ) {
170
			$oldTo = $to;
171
			Hooks::run( 'UserMailerSplitTo', [ &$to ] );
172
			if ( $oldTo != $to ) {
173
				$splitTo = array_diff( $oldTo, $to );
174
				$to = array_diff( $oldTo, $splitTo ); // ignore new addresses added in the hook
175
				// first send to non-split address list, then to split addresses one by one
176
				$status = Status::newGood();
177
				if ( $to ) {
178
					$status->merge( UserMailer::sendInternal(
179
						$to, $from, $subject, $body, $options ) );
180
				}
181
				foreach ( $splitTo as $newTo ) {
182
					$status->merge( UserMailer::sendInternal(
183
						[ $newTo ], $from, $subject, $body, $options ) );
184
				}
185
				return $status;
186
			}
187
		}
188
189
		return UserMailer::sendInternal( $to, $from, $subject, $body, $options );
190
	}
191
192
	/**
193
	 * Helper function fo UserMailer::send() which does the actual sending. It expects a $to
194
	 * list which the UserMailerSplitTo hook would not split further.
195
	 * @param MailAddress[] $to Array of recipients' email addresses
196
	 * @param MailAddress $from Sender's email
197
	 * @param string $subject Email's subject.
198
	 * @param string $body Email's text or Array of two strings to be the text and html bodies
199
	 * @param array $options:
0 ignored issues
show
There is no parameter named $options:. Did you maybe mean $options?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
200
	 * 		'replyTo' MailAddress
201
	 * 		'contentType' string default 'text/plain; charset=UTF-8'
202
	 * 		'headers' array Extra headers to set
203
	 *
204
	 * @throws MWException
205
	 * @throws Exception
206
	 * @return Status
207
	 */
208
	protected static function sendInternal(
209
		array $to,
210
		MailAddress $from,
211
		$subject,
212
		$body,
213
		$options = []
214
	) {
215
		global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams;
216
		$mime = null;
217
218
		$replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null;
219
		$contentType = isset( $options['contentType'] ) ?
220
			$options['contentType'] : 'text/plain; charset=UTF-8';
221
		$headers = isset( $options['headers'] ) ? $options['headers'] : [];
222
223
		// Allow transformation of content, such as encrypting/signing
224
		$error = false;
225 View Code Duplication
		if ( !Hooks::run( 'UserMailerTransformContent', [ $to, $from, &$body, &$error ] ) ) {
226
			if ( $error ) {
227
				return Status::newFatal( 'php-mail-error', $error );
228
			} else {
229
				return Status::newFatal( 'php-mail-error-unknown' );
230
			}
231
		}
232
233
		/**
234
		 * Forge email headers
235
		 * -------------------
236
		 *
237
		 * WARNING
238
		 *
239
		 * DO NOT add To: or Subject: headers at this step. They need to be
240
		 * handled differently depending upon the mailer we are going to use.
241
		 *
242
		 * To:
243
		 *  PHP mail() first argument is the mail receiver. The argument is
244
		 *  used as a recipient destination and as a To header.
245
		 *
246
		 *  PEAR mailer has a recipient argument which is only used to
247
		 *  send the mail. If no To header is given, PEAR will set it to
248
		 *  to 'undisclosed-recipients:'.
249
		 *
250
		 *  NOTE: To: is for presentation, the actual recipient is specified
251
		 *  by the mailer using the Rcpt-To: header.
252
		 *
253
		 * Subject:
254
		 *  PHP mail() second argument to pass the subject, passing a Subject
255
		 *  as an additional header will result in a duplicate header.
256
		 *
257
		 *  PEAR mailer should be passed a Subject header.
258
		 *
259
		 * -- hashar 20120218
260
		 */
261
262
		$headers['From'] = $from->toString();
263
		$returnPath = $from->address;
264
		$extraParams = $wgAdditionalMailParams;
265
266
		// Hook to generate custom VERP address for 'Return-Path'
267
		Hooks::run( 'UserMailerChangeReturnPath', [ $to, &$returnPath ] );
268
		// Add the envelope sender address using the -f command line option when PHP mail() is used.
269
		// Will default to the $from->address when the UserMailerChangeReturnPath hook fails and the
270
		// generated VERP address when the hook runs effectively.
271
		$extraParams .= ' -f ' . $returnPath;
272
273
		$headers['Return-Path'] = $returnPath;
274
275
		if ( $replyto ) {
276
			$headers['Reply-To'] = $replyto->toString();
277
		}
278
279
		$headers['Date'] = MWTimestamp::getLocalInstance()->format( 'r' );
280
		$headers['Message-ID'] = self::makeMsgId();
281
		$headers['X-Mailer'] = 'MediaWiki mailer';
282
		$headers['List-Unsubscribe'] = '<' . SpecialPage::getTitleFor( 'Preferences' )
283
			->getFullURL( '', false, PROTO_CANONICAL ) . '>';
284
285
		// Line endings need to be different on Unix and Windows due to
286
		// the bug described at https://core.trac.wordpress.org/ticket/2603
287
		$endl = PHP_EOL;
288
289
		if ( is_array( $body ) ) {
290
			// we are sending a multipart message
291
			wfDebug( "Assembling multipart mime email\n" );
292
			if ( !stream_resolve_include_path( 'Mail/mime.php' ) ) {
293
				wfDebug( "PEAR Mail_Mime package is not installed. Falling back to text email.\n" );
294
				// remove the html body for text email fall back
295
				$body = $body['text'];
296
			} else {
297
				// Check if pear/mail_mime is already loaded (via composer)
298
				if ( !class_exists( 'Mail_mime' ) ) {
299
					require_once 'Mail/mime.php';
300
				}
301
				if ( wfIsWindows() ) {
302
					$body['text'] = str_replace( "\n", "\r\n", $body['text'] );
303
					$body['html'] = str_replace( "\n", "\r\n", $body['html'] );
304
				}
305
				$mime = new Mail_mime( [
306
					'eol' => $endl,
307
					'text_charset' => 'UTF-8',
308
					'html_charset' => 'UTF-8'
309
				] );
310
				$mime->setTXTBody( $body['text'] );
311
				$mime->setHTMLBody( $body['html'] );
312
				$body = $mime->get(); // must call get() before headers()
313
				$headers = $mime->headers( $headers );
314
			}
315
		}
316
		if ( $mime === null ) {
317
			// sending text only, either deliberately or as a fallback
318
			if ( wfIsWindows() ) {
319
				$body = str_replace( "\n", "\r\n", $body );
320
			}
321
			$headers['MIME-Version'] = '1.0';
322
			$headers['Content-type'] = $contentType;
323
			$headers['Content-transfer-encoding'] = '8bit';
324
		}
325
326
		// allow transformation of MIME-encoded message
327 View Code Duplication
		if ( !Hooks::run( 'UserMailerTransformMessage',
328
			[ $to, $from, &$subject, &$headers, &$body, &$error ] )
329
		) {
330
			if ( $error ) {
331
				return Status::newFatal( 'php-mail-error', $error );
332
			} else {
333
				return Status::newFatal( 'php-mail-error-unknown' );
334
			}
335
		}
336
337
		$ret = Hooks::run( 'AlternateUserMailer', [ $headers, $to, $from, $subject, $body ] );
338
		if ( $ret === false ) {
339
			// the hook implementation will return false to skip regular mail sending
340
			return Status::newGood();
341
		} elseif ( $ret !== true ) {
342
			// the hook implementation will return a string to pass an error message
343
			return Status::newFatal( 'php-mail-error', $ret );
344
		}
345
346
		if ( is_array( $wgSMTP ) ) {
347
			// Check if pear/mail is already loaded (via composer)
348
			if ( !class_exists( 'Mail' ) ) {
349
				// PEAR MAILER
350
				if ( !stream_resolve_include_path( 'Mail.php' ) ) {
351
					throw new MWException( 'PEAR mail package is not installed' );
352
				}
353
				require_once 'Mail.php';
354
			}
355
356
			MediaWiki\suppressWarnings();
357
358
			// Create the mail object using the Mail::factory method
359
			$mail_object =& Mail::factory( 'smtp', $wgSMTP );
360 View Code Duplication
			if ( PEAR::isError( $mail_object ) ) {
361
				wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" );
362
				MediaWiki\restoreWarnings();
363
				return Status::newFatal( 'pear-mail-error', $mail_object->getMessage() );
364
			}
365
366
			wfDebug( "Sending mail via PEAR::Mail\n" );
367
368
			$headers['Subject'] = self::quotedPrintable( $subject );
369
370
			// When sending only to one recipient, shows it its email using To:
371
			if ( count( $to ) == 1 ) {
372
				$headers['To'] = $to[0]->toString();
373
			}
374
375
			// Split jobs since SMTP servers tends to limit the maximum
376
			// number of possible recipients.
377
			$chunks = array_chunk( $to, $wgEnotifMaxRecips );
378
			foreach ( $chunks as $chunk ) {
379
				$status = self::sendWithPear( $mail_object, $chunk, $headers, $body );
380
				// FIXME : some chunks might be sent while others are not!
381
				if ( !$status->isOK() ) {
382
					MediaWiki\restoreWarnings();
383
					return $status;
384
				}
385
			}
386
			MediaWiki\restoreWarnings();
387
			return Status::newGood();
388
		} else {
389
			// PHP mail()
390
			if ( count( $to ) > 1 ) {
391
				$headers['To'] = 'undisclosed-recipients:;';
392
			}
393
			$headers = self::arrayToHeaderString( $headers, $endl );
394
395
			wfDebug( "Sending mail via internal mail() function\n" );
396
397
			self::$mErrorString = '';
398
			$html_errors = ini_get( 'html_errors' );
399
			ini_set( 'html_errors', '0' );
400
			set_error_handler( 'UserMailer::errorHandler' );
401
402
			try {
403
				foreach ( $to as $recip ) {
404
					$sent = mail(
405
						$recip,
406
						self::quotedPrintable( $subject ),
407
						$body,
408
						$headers,
409
						$extraParams
410
					);
411
				}
412
			} catch ( Exception $e ) {
413
				restore_error_handler();
414
				throw $e;
415
			}
416
417
			restore_error_handler();
418
			ini_set( 'html_errors', $html_errors );
419
420
			if ( self::$mErrorString ) {
421
				wfDebug( "Error sending mail: " . self::$mErrorString . "\n" );
422
				return Status::newFatal( 'php-mail-error', self::$mErrorString );
423
			} elseif ( !$sent ) {
0 ignored issues
show
The variable $sent does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
424
				// mail function only tells if there's an error
425
				wfDebug( "Unknown error sending mail\n" );
426
				return Status::newFatal( 'php-mail-error-unknown' );
427
			} else {
428
				return Status::newGood();
429
			}
430
		}
431
	}
432
433
	/**
434
	 * Set the mail error message in self::$mErrorString
435
	 *
436
	 * @param int $code Error number
437
	 * @param string $string Error message
438
	 */
439
	static function errorHandler( $code, $string ) {
440
		self::$mErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string );
441
	}
442
443
	/**
444
	 * Strips bad characters from a header value to prevent PHP mail header injection attacks
445
	 * @param string $val String to be santizied
446
	 * @return string
447
	 */
448
	public static function sanitizeHeaderValue( $val ) {
449
		return strtr( $val, [ "\r" => '', "\n" => '' ] );
450
	}
451
452
	/**
453
	 * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name
454
	 * @param string $phrase
455
	 * @return string
456
	 */
457
	public static function rfc822Phrase( $phrase ) {
458
		// Remove line breaks
459
		$phrase = self::sanitizeHeaderValue( $phrase );
460
		// Remove quotes
461
		$phrase = str_replace( '"', '', $phrase );
462
		return '"' . $phrase . '"';
463
	}
464
465
	/**
466
	 * Converts a string into quoted-printable format
467
	 * @since 1.17
468
	 *
469
	 * From PHP5.3 there is a built in function quoted_printable_encode()
470
	 * This method does not duplicate that.
471
	 * This method is doing Q encoding inside encoded-words as defined by RFC 2047
472
	 * This is for email headers.
473
	 * The built in quoted_printable_encode() is for email bodies
474
	 * @param string $string
475
	 * @param string $charset
476
	 * @return string
477
	 */
478
	public static function quotedPrintable( $string, $charset = '' ) {
479
		// Probably incomplete; see RFC 2045
480
		if ( empty( $charset ) ) {
481
			$charset = 'UTF-8';
482
		}
483
		$charset = strtoupper( $charset );
484
		$charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ?
485
486
		$illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff=';
487
		$replace = $illegal . '\t ?_';
488
		if ( !preg_match( "/[$illegal]/", $string ) ) {
489
			return $string;
490
		}
491
		$out = "=?$charset?Q?";
492
		$out .= preg_replace_callback( "/([$replace])/",
493
			function ( $matches ) {
494
				return sprintf( "=%02X", ord( $matches[1] ) );
495
			},
496
			$string
497
		);
498
		$out .= '?=';
499
		return $out;
500
	}
501
}
502