Completed
Pull Request — master (#2278)
by ྅༻ Ǭɀħ
14:42
created

includes/functions-auth.php (5 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
 * Check for valid user via login form or stored cookie. Returns true or an error message
4
 *
5
 */
6
function yourls_is_valid_user() {
7
	// Allow plugins to short-circuit the whole function
8
	$pre = yourls_apply_filter( 'shunt_is_valid_user', null );
9
	if ( null !== $pre ) {
10
		return $pre;
11
	}
12
	
13
	// $unfiltered_valid : are credentials valid? Boolean value. It's "unfiltered" to allow plugins to eventually filter it.
14
	$unfiltered_valid = false;
15
16
	// Logout request
17
	if( isset( $_GET['action'] ) && $_GET['action'] == 'logout' ) {
18
		yourls_do_action( 'logout' );
19
		yourls_store_cookie( null );
20
		return yourls__( 'Logged out successfully' );
21
	}
22
	
23
	// Check cookies or login request. Login form has precedence.
24
25
	yourls_do_action( 'pre_login' );
26
27
	// Determine auth method and check credentials
28
	if
29
		// API only: Secure (no login or pwd) and time limited token
30
		// ?timestamp=12345678&signature=md5(totoblah12345678)
31
		( yourls_is_API() &&
32
		  isset( $_REQUEST['timestamp'] ) && !empty($_REQUEST['timestamp'] ) &&
33
		  isset( $_REQUEST['signature'] ) && !empty($_REQUEST['signature'] )
34
		)
35
		{
36
			yourls_do_action( 'pre_login_signature_timestamp' );
37
			$unfiltered_valid = yourls_check_signature_timestamp();
38
		}
39
		
40
	elseif
41
		// API only: Secure (no login or pwd)
42
		// ?signature=md5(totoblah)
43
		( yourls_is_API() &&
44
		  !isset( $_REQUEST['timestamp'] ) &&
45
		  isset( $_REQUEST['signature'] ) && !empty( $_REQUEST['signature'] )
46
		)
47
		{
48
			yourls_do_action( 'pre_login_signature' );
49
			$unfiltered_valid = yourls_check_signature();
50
		}
51
	
52
	elseif
53
		// API or normal: login with username & pwd
54
		( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] )
55
		  && !empty( $_REQUEST['username'] ) && !empty( $_REQUEST['password']  ) )
56
		{
57
			yourls_do_action( 'pre_login_username_password' );
58
			$unfiltered_valid = yourls_check_username_password();
59
		}
60
	
61
	elseif
62
		// Normal only: cookies
63
		( !yourls_is_API() && 
64
		  isset( $_COOKIE[ yourls_cookie_name() ] ) )
65
		{
66
			yourls_do_action( 'pre_login_cookie' );
67
			$unfiltered_valid = yourls_check_auth_cookie();
68
		}
69
	
70
	// Regardless of validity, allow plugins to filter the boolean and have final word
71
	$valid = yourls_apply_filter( 'is_valid_user', $unfiltered_valid );
72
73
	// Login for the win!
74
	if ( $valid ) {
75
		yourls_do_action( 'login' );
76
		
77
		// (Re)store encrypted cookie if needed
78
		if ( !yourls_is_API() ) {
79
			yourls_store_cookie( YOURLS_USER );
80
			
81
			// Login form : redirect to requested URL to avoid re-submitting the login form on page reload
82
			if( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
83
				$url = yourls_match_current_protocol(yourls_sanitize_url(sprintf("%s%s", $_SERVER['SERVER_NAME'], $_SERVER['REQUEST_URI'])));
84
				yourls_redirect( yourls_sanitize_url_safe($url) );
85
			}
86
		}
87
		
88
		// Login successful
89
		return true;
90
	}
91
	
92
	// Login failed
93
	yourls_do_action( 'login_failed' );
94
95
	if ( isset( $_REQUEST['username'] ) || isset( $_REQUEST['password'] ) ) {
96
		return yourls__( 'Invalid username or password' );
97
	} else {
98
		return yourls__( 'Please log in' );
99
	}
100
}
101
102
/**
103
 * Check auth against list of login=>pwd. Sets user if applicable, returns bool
104
 *
105
 */
106
function yourls_check_username_password() {
107
	global $yourls_user_passwords;
108
	if( isset( $yourls_user_passwords[ $_REQUEST['username'] ] ) && yourls_check_password_hash( $_REQUEST['username'], $_REQUEST['password'] ) ) {
109
		yourls_set_user( $_REQUEST['username'] );
110
		return true;
111
	}
112
	return false;
113
}
114
115
/**
116
 * Check a submitted password sent in plain text against stored password which can be a salted hash
117
 *
118
 */
119
function yourls_check_password_hash( $user, $submitted_password ) {
120
	global $yourls_user_passwords;
121
	
122
	if( !isset( $yourls_user_passwords[ $user ] ) )
123
		return false;
124
	
125
	if ( yourls_has_phpass_password( $user ) ) {
126
		// Stored password is hashed with phpass
127
		list( , $hash ) = explode( ':', $yourls_user_passwords[ $user ] );
128
		$hash = str_replace( '!', '$', $hash );
129
		return ( yourls_phpass_check( $submitted_password, $hash ) );
130
	} else if( yourls_has_md5_password( $user ) ) {
131
		// Stored password is a salted md5 hash: "md5:<$r = rand(10000,99999)>:<md5($r.'thepassword')>"
132
		list( , $salt, ) = explode( ':', $yourls_user_passwords[ $user ] );
133
		return( $yourls_user_passwords[ $user ] == 'md5:'.$salt.':'.md5( $salt . $submitted_password ) );
134
	} else {
135
		// Password stored in clear text
136
		return( $yourls_user_passwords[ $user ] == $submitted_password );
137
	}
138
}
139
140
/**
141
 * Overwrite plaintext passwords in config file with phpassed versions.
142
 *
143
 * @since 1.7
144
 * @param string $config_file Full path to file
145
 * @return true if overwrite was successful, an error message otherwise
146
 */
147
function yourls_hash_passwords_now( $config_file ) {
148
	if( !is_readable( $config_file ) )
149
		return 'cannot read file'; // not sure that can actually happen...
150
		
151
	if( !is_writable( $config_file ) )
152
		return 'cannot write file';	
153
	
154
	// Include file to read value of $yourls_user_passwords
155
	// Temporary suppress error reporting to avoid notices about redeclared constants
156
	$errlevel = error_reporting();
157
	error_reporting( 0 );
158
	require $config_file;
159
	error_reporting( $errlevel );
160
	
161
	$configdata = file_get_contents( $config_file );
162
	if( $configdata == false )
163
		return 'could not read file';
164
165
	$to_hash = 0; // keep track of number of passwords that need hashing
166
	foreach ( $yourls_user_passwords as $user => $password ) {
0 ignored issues
show
The variable $yourls_user_passwords does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
167
		if ( !yourls_has_phpass_password( $user ) && !yourls_has_md5_password( $user ) ) {
168
			$to_hash++;
169
			$hash = yourls_phpass_hash( $password );
170
			// PHP would interpret $ as a variable, so replace it in storage.
171
			$hash = str_replace( '$', '!', $hash );
172
			$quotes = "'" . '"';
173
			$pattern = "/[$quotes]${user}[$quotes]\s*=>\s*[$quotes]" . preg_quote( $password, '/' ) . "[$quotes]/";
174
			$replace = "'$user' => 'phpass:$hash' /* Password encrypted by YOURLS */ ";
175
			$count = 0;
176
			$configdata = preg_replace( $pattern, $replace, $configdata, -1, $count );
177
			// There should be exactly one replacement. Otherwise, fast fail.
178
			if ( $count != 1 ) {
179
				yourls_debug_log( "Problem with preg_replace for password hash of user $user" );
180
				return 'preg_replace problem';
181
			}
182
		}
183
	}
184
	
185
	if( $to_hash == 0 )
186
		return 0; // There was no password to encrypt
187
	
188
	$success = file_put_contents( $config_file, $configdata );
189
	if ( $success === FALSE ) {
190
		yourls_debug_log( 'Failed writing to ' . $config_file );
191
		return 'could not write file';
192
	}
193
	return true;
194
}
195
196
/**
197
 * Hash a password using phpass
198
 *
199
 * @since 1.7
200
 * @param string $password password to hash
201
 * @return string hashed password
202
 */
203
function yourls_phpass_hash( $password ) {
204
	$hasher = yourls_phpass_instance();
205
	return $hasher->HashPassword( $password );
206
}
207
208
/**
209
 * Check a clear password against a phpass hash
210
 *
211
 * @since 1.7
212
 * @param string $password clear (eg submitted in a form) password
213
 * @param string $hash hash supposedly generated by phpass
214
 * @return bool true if the hash matches the password once hashed by phpass, false otherwise
215
 */
216
function yourls_phpass_check( $password, $hash ) {
217
	$hasher = yourls_phpass_instance();
218
	return $hasher->CheckPassword( $password, $hash );
219
}
220
221
/**
222
 * Helper function: create new instance or return existing instance of phpass class
223
 *
224
 * @since 1.7
225
 * @param int $iteration iteration count - 8 is default in phpass
226
 * @param bool $portable flag to force portable (cross platform and system independant) hashes - false to use whatever the system can do best
227
 * @return object a PasswordHash instance
228
 */
229
function yourls_phpass_instance( $iteration = 8, $portable = false ) {
230
	$iteration = yourls_apply_filter( 'phpass_new_instance_iteration', $iteration );
231
	$portable  = yourls_apply_filter( 'phpass_new_instance_portable', $portable );
232
    
233
	static $instance = false;
234
	if( $instance == false ) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
235
		$instance = new \Ozh\Phpass\PasswordHash( $iteration, $portable );
236
	}
237
	
238
	return $instance;
239
}
240
241
242
/**
243
 * Check to see if any passwords are stored as cleartext.
244
 * 
245
 * @since 1.7
246
 * @return bool true if any passwords are cleartext
247
 */
248
function yourls_has_cleartext_passwords() {
249
	global $yourls_user_passwords;
250
	foreach ( $yourls_user_passwords as $user => $pwdata ) {
251
		if ( !yourls_has_md5_password( $user ) && !yourls_has_phpass_password( $user ) ) {
252
			return true;
253
		}
254
	}
255
	return false;
256
}
257
258
/**
259
 * Check if a user has a hashed password
260
 *
261
 * Check if a user password is 'md5:[38 chars]'.
262
 * TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
263
 *
264
 * @since 1.7
265
 * @param string $user user login
266
 * @return bool true if password hashed, false otherwise
267
 */
268
function yourls_has_md5_password( $user ) {
269
	global $yourls_user_passwords;
270
	return(    isset( $yourls_user_passwords[ $user ] )
271
	        && substr( $yourls_user_passwords[ $user ], 0, 4 ) == 'md5:'
272
		    && strlen( $yourls_user_passwords[ $user ] ) == 42 // http://www.google.com/search?q=the+answer+to+life+the+universe+and+everything
273
		   );
274
}
275
276
/**
277
 * Check if a user's password is hashed with PHPASS.
278
 *
279
 * Check if a user password is 'phpass:[lots of chars]'.
280
 * TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
281
 *
282
 * @since 1.7
283
 * @param string $user user login
284
 * @return bool true if password hashed with PHPASS, otherwise false
285
 */
286
function yourls_has_phpass_password( $user ) {
287
	global $yourls_user_passwords;
288
	return( isset( $yourls_user_passwords[ $user ] )
289
	        && substr( $yourls_user_passwords[ $user ], 0, 7 ) == 'phpass:'
290
	);
291
}
292
293
/**
294
 * Check auth against encrypted COOKIE data. Sets user if applicable, returns bool
295
 *
296
 */
297
function yourls_check_auth_cookie() {
298
	global $yourls_user_passwords;
299
	foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
300
		if ( yourls_salt( $valid_user ) == $_COOKIE[ yourls_cookie_name() ] ) {
301
			yourls_set_user( $valid_user );
302
			return true;
303
		}
304
	}
305
	return false;
306
}
307
308
/**
309
 * Check auth against signature and timestamp. Sets user if applicable, returns bool
310
 *
311
 *
312
 * @since 1.4.1
313
 * @return bool False if signature or timestamp missing or invalid, true if valid
314
 */
315
function yourls_check_signature_timestamp() {
316
    if(   !isset( $_REQUEST['signature'] ) OR empty( $_REQUEST['signature'] )
317
       OR !isset( $_REQUEST['timestamp'] ) OR empty( $_REQUEST['timestamp'] )
318
    )
319
        return false;
320
321
	// Timestamp in PHP : time()
322
	// Timestamp in JS: parseInt(new Date().getTime() / 1000)
323
    
324
	// Check signature & timestamp against all possible users
325
	global $yourls_user_passwords;
326
	foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
327
		if (
328
			(
329
				md5( $_REQUEST['timestamp'].yourls_auth_signature( $valid_user ) ) == $_REQUEST['signature']
0 ignored issues
show
$valid_user is of type integer|string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
330
				or
331
				md5( yourls_auth_signature( $valid_user ).$_REQUEST['timestamp'] ) == $_REQUEST['signature']
0 ignored issues
show
$valid_user is of type integer|string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
332
			)
333
			&&
334
			yourls_check_timestamp( $_REQUEST['timestamp'] )
335
			) {
336
			yourls_set_user( $valid_user );
337
			return true;
338
		}
339
	}
340
341
    // Signature doesn't match known user
342
	return false;
343
}
344
345
/**
346
 * Check auth against signature. Sets user if applicable, returns bool
347
 *
348
 * @since 1.4.1
349
 * @return bool False if signature missing or invalid, true if valid
350
 */
351
function yourls_check_signature() {
352
    if( !isset( $_REQUEST['signature'] ) OR empty( $_REQUEST['signature'] ) )
353
        return false;
354
    
355
	// Check signature against all possible users
356
    global $yourls_user_passwords;
357
	foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
358
		if ( yourls_auth_signature( $valid_user ) == $_REQUEST['signature'] ) {
0 ignored issues
show
$valid_user is of type integer|string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
359
			yourls_set_user( $valid_user );
360
			return true;
361
		}
362
	}
363
    
364
    // Signature doesn't match known user
365
	return false;
366
}
367
368
/**
369
 * Generate secret signature hash
370
 *
371
 */
372
function yourls_auth_signature( $username = false ) {
373
	if( !$username && defined('YOURLS_USER') ) {
374
		$username = YOURLS_USER;
375
	}
376
	return ( $username ? substr( yourls_salt( $username ), 0, 10 ) : 'Cannot generate auth signature: no username' );
377
}
378
379
/**
380
 * Check if timestamp is not too old
381
 *
382
 */
383
function yourls_check_timestamp( $time ) {
384
	$now = time();
385
	// Allow timestamp to be a little in the future or the past -- see Issue 766
386
	return yourls_apply_filter( 'check_timestamp', abs( $now - $time ) < YOURLS_NONCE_LIFE, $time );
387
}
388
389
/**
390
 * Store new cookie. No $user will delete the cookie.
391
 *
392
 * @param mixed $user  String, user login, or null to delete cookie
393
 */
394
function yourls_store_cookie( $user = null ) {
395
396
    // No user will delete the cookie with a cookie time from the past
397
	if( !$user ) {
398
		$time = time() - 3600;
399
	} else {
400
		$time = time() + YOURLS_COOKIE_LIFE;
401
	}
402
	
403
	$domain   = yourls_apply_filter( 'setcookie_domain',   parse_url( YOURLS_SITE, PHP_URL_HOST ) );
404
	$secure   = yourls_apply_filter( 'setcookie_secure',   yourls_is_ssl() );
405
	$httponly = yourls_apply_filter( 'setcookie_httponly', true );
406
407
	// Some browsers refuse to store localhost cookie
408
	if ( $domain == 'localhost' ) 
409
		$domain = '';
410
   
411
    if ( !headers_sent( $filename, $linenum ) ) {
412
        setcookie( yourls_cookie_name(), yourls_salt( $user ), $time, '/', $domain, $secure, $httponly );
413
	} else {
414
		// For some reason cookies were not stored: action to be able to debug that
415
		yourls_do_action( 'setcookie_failed', $user );
416
        yourls_debug_log( "Could not store cookie: headers already sent in $filename on line $linenum" );
417
	}
418
}
419
420
/**
421
 * Set user name
422
 *
423
 */
424
function yourls_set_user( $user ) {
425
	if( !defined( 'YOURLS_USER' ) )
426
		define( 'YOURLS_USER', $user );
427
}
428
429
/**
430
 * Get YOURLS cookie name
431
 *
432
 * The name is unique for each install, to prevent mismatch between sho.rt and very.sho.rt -- see #1673
433
 *
434
 * TODO: when multi user is implemented, the whole cookie stuff should be reworked to allow storing multiple users
435
 *
436
 * @since 1.7.1
437
 * @return string  unique cookie name for a given YOURLS site
438
 */
439
function yourls_cookie_name() {
440
    return 'yourls_' . yourls_salt( YOURLS_SITE );
441
}
442