Issues (847)

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.

inc/auth.php (1 issue)

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
 * Authentication library
4
 *
5
 * Including this file will automatically try to login
6
 * a user by calling auth_login()
7
 *
8
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9
 * @author     Andreas Gohr <[email protected]>
10
 */
11
12
use dokuwiki\Extension\AuthPlugin;
13
use dokuwiki\Extension\Event;
14
use dokuwiki\Extension\PluginController;
15
use dokuwiki\PassHash;
16
use dokuwiki\Subscriptions\RegistrationSubscriptionSender;
17
18
/**
19
 * Initialize the auth system.
20
 *
21
 * This function is automatically called at the end of init.php
22
 *
23
 * This used to be the main() of the auth.php
24
 *
25
 * @todo backend loading maybe should be handled by the class autoloader
26
 * @todo maybe split into multiple functions at the XXX marked positions
27
 * @triggers AUTH_LOGIN_CHECK
28
 * @return bool
29
 */
30
function auth_setup() {
31
    global $conf;
32
    /* @var AuthPlugin $auth */
33
    global $auth;
34
    /* @var Input $INPUT */
35
    global $INPUT;
36
    global $AUTH_ACL;
37
    global $lang;
38
    /* @var PluginController $plugin_controller */
39
    global $plugin_controller;
40
    $AUTH_ACL = array();
41
42
    if(!$conf['useacl']) return false;
43
44
    // try to load auth backend from plugins
45
    foreach ($plugin_controller->getList('auth') as $plugin) {
46
        if ($conf['authtype'] === $plugin) {
47
            $auth = $plugin_controller->load('auth', $plugin);
48
            break;
49
        }
50
    }
51
52
    if(!isset($auth) || !$auth){
53
        msg($lang['authtempfail'], -1);
54
        return false;
55
    }
56
57
    if ($auth->success == false) {
58
        // degrade to unauthenticated user
59
        unset($auth);
60
        auth_logoff();
61
        msg($lang['authtempfail'], -1);
62
        return false;
63
    }
64
65
    // do the login either by cookie or provided credentials XXX
66
    $INPUT->set('http_credentials', false);
67
    if(!$conf['rememberme']) $INPUT->set('r', false);
68
69
    // handle renamed HTTP_AUTHORIZATION variable (can happen when a fix like
70
    // the one presented at
71
    // http://www.besthostratings.com/articles/http-auth-php-cgi.html is used
72
    // for enabling HTTP authentication with CGI/SuExec)
73
    if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
74
        $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
75
    // streamline HTTP auth credentials (IIS/rewrite -> mod_php)
76
    if(isset($_SERVER['HTTP_AUTHORIZATION'])) {
77
        list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
78
            explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
79
    }
80
81
    // if no credentials were given try to use HTTP auth (for SSO)
82
    if(!$INPUT->str('u') && empty($_COOKIE[DOKU_COOKIE]) && !empty($_SERVER['PHP_AUTH_USER'])) {
83
        $INPUT->set('u', $_SERVER['PHP_AUTH_USER']);
84
        $INPUT->set('p', $_SERVER['PHP_AUTH_PW']);
85
        $INPUT->set('http_credentials', true);
86
    }
87
88
    // apply cleaning (auth specific user names, remove control chars)
89
    if (true === $auth->success) {
90
        $INPUT->set('u', $auth->cleanUser(stripctl($INPUT->str('u'))));
91
        $INPUT->set('p', stripctl($INPUT->str('p')));
92
    }
93
94
    $ok = null;
95
    if (!is_null($auth) && $auth->canDo('external')) {
96
        $ok = $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r'));
97
    }
98
99
    if ($ok === null) {
100
        // external trust mechanism not in place, or returns no result,
101
        // then attempt auth_login
102
        $evdata = array(
103
            'user'     => $INPUT->str('u'),
104
            'password' => $INPUT->str('p'),
105
            'sticky'   => $INPUT->bool('r'),
106
            'silent'   => $INPUT->bool('http_credentials')
107
        );
108
        Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
109
    }
110
111
    //load ACL into a global array XXX
112
    $AUTH_ACL = auth_loadACL();
113
114
    return true;
115
}
116
117
/**
118
 * Loads the ACL setup and handle user wildcards
119
 *
120
 * @author Andreas Gohr <[email protected]>
121
 *
122
 * @return array
123
 */
124
function auth_loadACL() {
125
    global $config_cascade;
126
    global $USERINFO;
127
    /* @var Input $INPUT */
128
    global $INPUT;
129
130
    if(!is_readable($config_cascade['acl']['default'])) return array();
131
132
    $acl = file($config_cascade['acl']['default']);
133
134
    $out = array();
135
    foreach($acl as $line) {
136
        $line = trim($line);
137
        if(empty($line) || ($line[0] == '#')) continue; // skip blank lines & comments
138
        list($id,$rest) = preg_split('/[ \t]+/',$line,2);
139
140
        // substitute user wildcard first (its 1:1)
141
        if(strstr($line, '%USER%')){
142
            // if user is not logged in, this ACL line is meaningless - skip it
143
            if (!$INPUT->server->has('REMOTE_USER')) continue;
144
145
            $id   = str_replace('%USER%',cleanID($INPUT->server->str('REMOTE_USER')),$id);
146
            $rest = str_replace('%USER%',auth_nameencode($INPUT->server->str('REMOTE_USER')),$rest);
147
        }
148
149
        // substitute group wildcard (its 1:m)
150
        if(strstr($line, '%GROUP%')){
151
            // if user is not logged in, grps is empty, no output will be added (i.e. skipped)
152
            if(isset($USERINFO['grps'])){
153
                foreach((array) $USERINFO['grps'] as $grp){
154
                    $nid   = str_replace('%GROUP%',cleanID($grp),$id);
155
                    $nrest = str_replace('%GROUP%','@'.auth_nameencode($grp),$rest);
156
                    $out[] = "$nid\t$nrest";
157
                }
158
            }
159
        } else {
160
            $out[] = "$id\t$rest";
161
        }
162
    }
163
164
    return $out;
165
}
166
167
/**
168
 * Event hook callback for AUTH_LOGIN_CHECK
169
 *
170
 * @param array $evdata
171
 * @return bool
172
 */
173
function auth_login_wrapper($evdata) {
174
    return auth_login(
175
        $evdata['user'],
176
        $evdata['password'],
177
        $evdata['sticky'],
178
        $evdata['silent']
179
    );
180
}
181
182
/**
183
 * This tries to login the user based on the sent auth credentials
184
 *
185
 * The authentication works like this: if a username was given
186
 * a new login is assumed and user/password are checked. If they
187
 * are correct the password is encrypted with blowfish and stored
188
 * together with the username in a cookie - the same info is stored
189
 * in the session, too. Additonally a browserID is stored in the
190
 * session.
191
 *
192
 * If no username was given the cookie is checked: if the username,
193
 * crypted password and browserID match between session and cookie
194
 * no further testing is done and the user is accepted
195
 *
196
 * If a cookie was found but no session info was availabe the
197
 * blowfish encrypted password from the cookie is decrypted and
198
 * together with username rechecked by calling this function again.
199
 *
200
 * On a successful login $_SERVER[REMOTE_USER] and $USERINFO
201
 * are set.
202
 *
203
 * @author  Andreas Gohr <[email protected]>
204
 *
205
 * @param   string  $user    Username
206
 * @param   string  $pass    Cleartext Password
207
 * @param   bool    $sticky  Cookie should not expire
208
 * @param   bool    $silent  Don't show error on bad auth
209
 * @return  bool             true on successful auth
210
 */
211
function auth_login($user, $pass, $sticky = false, $silent = false) {
212
    global $USERINFO;
213
    global $conf;
214
    global $lang;
215
    /* @var AuthPlugin $auth */
216
    global $auth;
217
    /* @var Input $INPUT */
218
    global $INPUT;
219
220
    $sticky ? $sticky = true : $sticky = false; //sanity check
221
222
    if(!$auth) return false;
223
224
    if(!empty($user)) {
225
        //usual login
226
        if(!empty($pass) && $auth->checkPass($user, $pass)) {
227
            // make logininfo globally available
228
            $INPUT->server->set('REMOTE_USER', $user);
229
            $secret                 = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
230
            auth_setCookie($user, auth_encrypt($pass, $secret), $sticky);
231
            return true;
232
        } else {
233
            //invalid credentials - log off
234
            if(!$silent) {
235
                http_status(403, 'Login failed');
236
                msg($lang['badlogin'], -1);
237
            }
238
            auth_logoff();
239
            return false;
240
        }
241
    } else {
242
        // read cookie information
243
        list($user, $sticky, $pass) = auth_getCookie();
244
        if($user && $pass) {
245
            // we got a cookie - see if we can trust it
246
247
            // get session info
248
            if (isset($_SESSION[DOKU_COOKIE])) {
249
                $session = $_SESSION[DOKU_COOKIE]['auth'];
250
                if (isset($session) &&
251
                    $auth->useSessionCache($user) &&
252
                    ($session['time'] >= time() - $conf['auth_security_timeout']) &&
253
                    ($session['user'] == $user) &&
254
                    ($session['pass'] == sha1($pass)) && //still crypted
255
                    ($session['buid'] == auth_browseruid())
256
                ) {
257
258
                    // he has session, cookie and browser right - let him in
259
                    $INPUT->server->set('REMOTE_USER', $user);
260
                    $USERINFO = $session['info']; //FIXME move all references to session
261
                    return true;
262
                }
263
            }
264
            // no we don't trust it yet - recheck pass but silent
265
            $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
266
            $pass   = auth_decrypt($pass, $secret);
267
            return auth_login($user, $pass, $sticky, true);
268
        }
269
    }
270
    //just to be sure
271
    auth_logoff(true);
272
    return false;
273
}
274
275
/**
276
 * Builds a pseudo UID from browser and IP data
277
 *
278
 * This is neither unique nor unfakable - still it adds some
279
 * security. Using the first part of the IP makes sure
280
 * proxy farms like AOLs are still okay.
281
 *
282
 * @author  Andreas Gohr <[email protected]>
283
 *
284
 * @return  string  a MD5 sum of various browser headers
285
 */
286
function auth_browseruid() {
287
    /* @var Input $INPUT */
288
    global $INPUT;
289
290
    $ip  = clientIP(true);
291
    $uid = '';
292
    $uid .= $INPUT->server->str('HTTP_USER_AGENT');
293
    $uid .= $INPUT->server->str('HTTP_ACCEPT_CHARSET');
294
    $uid .= substr($ip, 0, strpos($ip, '.'));
295
    $uid = \dokuwiki\Utf8\PhpString::strtolower($uid);
296
    return md5($uid);
297
}
298
299
/**
300
 * Creates a random key to encrypt the password in cookies
301
 *
302
 * This function tries to read the password for encrypting
303
 * cookies from $conf['metadir'].'/_htcookiesalt'
304
 * if no such file is found a random key is created and
305
 * and stored in this file.
306
 *
307
 * @author  Andreas Gohr <[email protected]>
308
 *
309
 * @param   bool $addsession if true, the sessionid is added to the salt
310
 * @param   bool $secure     if security is more important than keeping the old value
311
 * @return  string
312
 */
313
function auth_cookiesalt($addsession = false, $secure = false) {
314
    if (defined('SIMPLE_TEST')) {
315
        return 'test';
316
    }
317
    global $conf;
318
    $file = $conf['metadir'].'/_htcookiesalt';
319
    if ($secure || !file_exists($file)) {
320
        $file = $conf['metadir'].'/_htcookiesalt2';
321
    }
322
    $salt = io_readFile($file);
323
    if(empty($salt)) {
324
        $salt = bin2hex(auth_randombytes(64));
325
        io_saveFile($file, $salt);
326
    }
327
    if($addsession) {
328
        $salt .= session_id();
329
    }
330
    return $salt;
331
}
332
333
/**
334
 * Return cryptographically secure random bytes.
335
 *
336
 * @author Niklas Keller <[email protected]>
337
 *
338
 * @param int $length number of bytes
339
 * @return string cryptographically secure random bytes
340
 */
341
function auth_randombytes($length) {
342
    return random_bytes($length);
343
}
344
345
/**
346
 * Cryptographically secure random number generator.
347
 *
348
 * @author Niklas Keller <[email protected]>
349
 *
350
 * @param int $min
351
 * @param int $max
352
 * @return int
353
 */
354
function auth_random($min, $max) {
355
    return random_int($min, $max);
356
}
357
358
/**
359
 * Encrypt data using the given secret using AES
360
 *
361
 * The mode is CBC with a random initialization vector, the key is derived
362
 * using pbkdf2.
363
 *
364
 * @param string $data   The data that shall be encrypted
365
 * @param string $secret The secret/password that shall be used
366
 * @return string The ciphertext
367
 */
368
function auth_encrypt($data, $secret) {
369
    $iv     = auth_randombytes(16);
370
    $cipher = new \phpseclib\Crypt\AES();
371
    $cipher->setPassword($secret);
372
373
    /*
374
    this uses the encrypted IV as IV as suggested in
375
    http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C
376
    for unique but necessarily random IVs. The resulting ciphertext is
377
    compatible to ciphertext that was created using a "normal" IV.
378
    */
379
    return $cipher->encrypt($iv.$data);
380
}
381
382
/**
383
 * Decrypt the given AES ciphertext
384
 *
385
 * The mode is CBC, the key is derived using pbkdf2
386
 *
387
 * @param string $ciphertext The encrypted data
388
 * @param string $secret     The secret/password that shall be used
389
 * @return string The decrypted data
390
 */
391
function auth_decrypt($ciphertext, $secret) {
392
    $iv     = substr($ciphertext, 0, 16);
393
    $cipher = new \phpseclib\Crypt\AES();
394
    $cipher->setPassword($secret);
395
    $cipher->setIV($iv);
396
397
    return $cipher->decrypt(substr($ciphertext, 16));
398
}
399
400
/**
401
 * Log out the current user
402
 *
403
 * This clears all authentication data and thus log the user
404
 * off. It also clears session data.
405
 *
406
 * @author  Andreas Gohr <[email protected]>
407
 *
408
 * @param bool $keepbc - when true, the breadcrumb data is not cleared
409
 */
410
function auth_logoff($keepbc = false) {
411
    global $conf;
412
    global $USERINFO;
413
    /* @var AuthPlugin $auth */
414
    global $auth;
415
    /* @var Input $INPUT */
416
    global $INPUT;
417
418
    // make sure the session is writable (it usually is)
419
    @session_start();
420
421
    if(isset($_SESSION[DOKU_COOKIE]['auth']['user']))
422
        unset($_SESSION[DOKU_COOKIE]['auth']['user']);
423
    if(isset($_SESSION[DOKU_COOKIE]['auth']['pass']))
424
        unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
425
    if(isset($_SESSION[DOKU_COOKIE]['auth']['info']))
426
        unset($_SESSION[DOKU_COOKIE]['auth']['info']);
427
    if(!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc']))
428
        unset($_SESSION[DOKU_COOKIE]['bc']);
429
    $INPUT->server->remove('REMOTE_USER');
430
    $USERINFO = null; //FIXME
431
432
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
433
    setcookie(DOKU_COOKIE, '', time() - 600000, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
434
435
    if($auth) $auth->logOff();
436
}
437
438
/**
439
 * Check if a user is a manager
440
 *
441
 * Should usually be called without any parameters to check the current
442
 * user.
443
 *
444
 * The info is available through $INFO['ismanager'], too
445
 *
446
 * @param string $user Username
447
 * @param array $groups List of groups the user is in
448
 * @param bool $adminonly when true checks if user is admin
449
 * @param bool $recache set to true to refresh the cache
450
 * @return bool
451
 * @see    auth_isadmin
452
 *
453
 * @author Andreas Gohr <[email protected]>
454
 */
455
function auth_ismanager($user = null, $groups = null, $adminonly = false, $recache=false) {
456
    global $conf;
457
    global $USERINFO;
458
    /* @var AuthPlugin $auth */
459
    global $auth;
460
    /* @var Input $INPUT */
461
    global $INPUT;
462
463
464
    if(!$auth) return false;
465
    if(is_null($user)) {
466
        if(!$INPUT->server->has('REMOTE_USER')) {
467
            return false;
468
        } else {
469
            $user = $INPUT->server->str('REMOTE_USER');
470
        }
471
    }
472
    if (is_null($groups)) {
473
        // checking the logged in user, or another one?
474
        if ($USERINFO && $user === $INPUT->server->str('REMOTE_USER')) {
475
            $groups =  (array) $USERINFO['grps'];
476
        } else {
477
            $groups = $auth->getUserData($user);
478
            $groups = $groups ? $groups['grps'] : [];
479
        }
480
    }
481
482
    // prefer cached result
483
    static $cache = [];
484
    $cachekey = serialize([$user, $adminonly, $groups]);
485
    if (!isset($cache[$cachekey]) || $recache) {
486
        // check superuser match
487
        $ok = auth_isMember($conf['superuser'], $user, $groups);
488
489
        // check managers
490
        if (!$ok && !$adminonly) {
491
            $ok = auth_isMember($conf['manager'], $user, $groups);
492
        }
493
494
        $cache[$cachekey] = $ok;
495
    }
496
497
    return $cache[$cachekey];
498
}
499
500
/**
501
 * Check if a user is admin
502
 *
503
 * Alias to auth_ismanager with adminonly=true
504
 *
505
 * The info is available through $INFO['isadmin'], too
506
 *
507
 * @param string $user Username
508
 * @param array $groups List of groups the user is in
509
 * @param bool $recache set to true to refresh the cache
510
 * @return bool
511
 * @author Andreas Gohr <[email protected]>
512
 * @see auth_ismanager()
513
 *
514
 */
515
function auth_isadmin($user = null, $groups = null, $recache=false) {
516
    return auth_ismanager($user, $groups, true, $recache);
517
}
518
519
/**
520
 * Match a user and his groups against a comma separated list of
521
 * users and groups to determine membership status
522
 *
523
 * Note: all input should NOT be nameencoded.
524
 *
525
 * @param string $memberlist commaseparated list of allowed users and groups
526
 * @param string $user       user to match against
527
 * @param array  $groups     groups the user is member of
528
 * @return bool       true for membership acknowledged
529
 */
530
function auth_isMember($memberlist, $user, array $groups) {
531
    /* @var AuthPlugin $auth */
532
    global $auth;
533
    if(!$auth) return false;
534
535
    // clean user and groups
536
    if(!$auth->isCaseSensitive()) {
537
        $user   = \dokuwiki\Utf8\PhpString::strtolower($user);
538
        $groups = array_map([\dokuwiki\Utf8\PhpString::class, 'strtolower'], $groups);
539
    }
540
    $user   = $auth->cleanUser($user);
541
    $groups = array_map(array($auth, 'cleanGroup'), $groups);
542
543
    // extract the memberlist
544
    $members = explode(',', $memberlist);
545
    $members = array_map('trim', $members);
546
    $members = array_unique($members);
547
    $members = array_filter($members);
548
549
    // compare cleaned values
550
    foreach($members as $member) {
551
        if($member == '@ALL' ) return true;
552
        if(!$auth->isCaseSensitive()) $member = \dokuwiki\Utf8\PhpString::strtolower($member);
553
        if($member[0] == '@') {
554
            $member = $auth->cleanGroup(substr($member, 1));
555
            if(in_array($member, $groups)) return true;
556
        } else {
557
            $member = $auth->cleanUser($member);
558
            if($member == $user) return true;
559
        }
560
    }
561
562
    // still here? not a member!
563
    return false;
564
}
565
566
/**
567
 * Convinience function for auth_aclcheck()
568
 *
569
 * This checks the permissions for the current user
570
 *
571
 * @author  Andreas Gohr <[email protected]>
572
 *
573
 * @param  string  $id  page ID (needs to be resolved and cleaned)
574
 * @return int          permission level
575
 */
576
function auth_quickaclcheck($id) {
577
    global $conf;
578
    global $USERINFO;
579
    /* @var Input $INPUT */
580
    global $INPUT;
581
    # if no ACL is used always return upload rights
582
    if(!$conf['useacl']) return AUTH_UPLOAD;
583
    return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), is_array($USERINFO) ? $USERINFO['grps'] : array());
584
}
585
586
/**
587
 * Returns the maximum rights a user has for the given ID or its namespace
588
 *
589
 * @author  Andreas Gohr <[email protected]>
590
 *
591
 * @triggers AUTH_ACL_CHECK
592
 * @param  string       $id     page ID (needs to be resolved and cleaned)
593
 * @param  string       $user   Username
594
 * @param  array|null   $groups Array of groups the user is in
595
 * @return int             permission level
596
 */
597
function auth_aclcheck($id, $user, $groups) {
598
    $data = array(
599
        'id'     => $id,
600
        'user'   => $user,
601
        'groups' => $groups
602
    );
603
604
    return Event::createAndTrigger('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb');
605
}
606
607
/**
608
 * default ACL check method
609
 *
610
 * DO NOT CALL DIRECTLY, use auth_aclcheck() instead
611
 *
612
 * @author  Andreas Gohr <[email protected]>
613
 *
614
 * @param  array $data event data
615
 * @return int   permission level
616
 */
617
function auth_aclcheck_cb($data) {
618
    $id     =& $data['id'];
619
    $user   =& $data['user'];
620
    $groups =& $data['groups'];
621
622
    global $conf;
623
    global $AUTH_ACL;
624
    /* @var AuthPlugin $auth */
625
    global $auth;
626
627
    // if no ACL is used always return upload rights
628
    if(!$conf['useacl']) return AUTH_UPLOAD;
629
    if(!$auth) return AUTH_NONE;
630
631
    //make sure groups is an array
632
    if(!is_array($groups)) $groups = array();
633
634
    //if user is superuser or in superusergroup return 255 (acl_admin)
635
    if(auth_isadmin($user, $groups)) {
636
        return AUTH_ADMIN;
637
    }
638
639
    if(!$auth->isCaseSensitive()) {
640
        $user   = \dokuwiki\Utf8\PhpString::strtolower($user);
641
        $groups = array_map('utf8_strtolower', $groups);
642
    }
643
    $user   = auth_nameencode($auth->cleanUser($user));
644
    $groups = array_map(array($auth, 'cleanGroup'), (array) $groups);
645
646
    //prepend groups with @ and nameencode
647
    foreach($groups as &$group) {
648
        $group = '@'.auth_nameencode($group);
649
    }
650
651
    $ns   = getNS($id);
652
    $perm = -1;
653
654
    //add ALL group
655
    $groups[] = '@ALL';
656
657
    //add User
658
    if($user) $groups[] = $user;
659
660
    //check exact match first
661
    $matches = preg_grep('/^'.preg_quote($id, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
662
    if(count($matches)) {
663
        foreach($matches as $match) {
664
            $match = preg_replace('/#.*$/', '', $match); //ignore comments
665
            $acl   = preg_split('/[ \t]+/', $match);
666
            if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
667
                $acl[1] = \dokuwiki\Utf8\PhpString::strtolower($acl[1]);
668
            }
669
            if(!in_array($acl[1], $groups)) {
670
                continue;
671
            }
672
            if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
673
            if($acl[2] > $perm) {
674
                $perm = $acl[2];
675
            }
676
        }
677
        if($perm > -1) {
678
            //we had a match - return it
679
            return (int) $perm;
680
        }
681
    }
682
683
    //still here? do the namespace checks
684
    if($ns) {
685
        $path = $ns.':*';
686
    } else {
687
        $path = '*'; //root document
688
    }
689
690
    do {
691
        $matches = preg_grep('/^'.preg_quote($path, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
692
        if(count($matches)) {
693
            foreach($matches as $match) {
694
                $match = preg_replace('/#.*$/', '', $match); //ignore comments
695
                $acl   = preg_split('/[ \t]+/', $match);
696
                if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
697
                    $acl[1] = \dokuwiki\Utf8\PhpString::strtolower($acl[1]);
698
                }
699
                if(!in_array($acl[1], $groups)) {
700
                    continue;
701
                }
702
                if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
703
                if($acl[2] > $perm) {
704
                    $perm = $acl[2];
705
                }
706
            }
707
            //we had a match - return it
708
            if($perm != -1) {
709
                return (int) $perm;
710
            }
711
        }
712
        //get next higher namespace
713
        $ns = getNS($ns);
714
715
        if($path != '*') {
716
            $path = $ns.':*';
717
            if($path == ':*') $path = '*';
718
        } else {
719
            //we did this already
720
            //looks like there is something wrong with the ACL
721
            //break here
722
            msg('No ACL setup yet! Denying access to everyone.');
723
            return AUTH_NONE;
724
        }
725
    } while(1); //this should never loop endless
726
    return AUTH_NONE;
727
}
728
729
/**
730
 * Encode ASCII special chars
731
 *
732
 * Some auth backends allow special chars in their user and groupnames
733
 * The special chars are encoded with this function. Only ASCII chars
734
 * are encoded UTF-8 multibyte are left as is (different from usual
735
 * urlencoding!).
736
 *
737
 * Decoding can be done with rawurldecode
738
 *
739
 * @author Andreas Gohr <[email protected]>
740
 * @see rawurldecode()
741
 *
742
 * @param string $name
743
 * @param bool $skip_group
744
 * @return string
745
 */
746
function auth_nameencode($name, $skip_group = false) {
747
    global $cache_authname;
748
    $cache =& $cache_authname;
749
    $name  = (string) $name;
750
751
    // never encode wildcard FS#1955
752
    if($name == '%USER%') return $name;
753
    if($name == '%GROUP%') return $name;
754
755
    if(!isset($cache[$name][$skip_group])) {
756
        if($skip_group && $name[0] == '@') {
757
            $cache[$name][$skip_group] = '@'.preg_replace_callback(
758
                '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
759
                'auth_nameencode_callback', substr($name, 1)
760
            );
761
        } else {
762
            $cache[$name][$skip_group] = preg_replace_callback(
763
                '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
764
                'auth_nameencode_callback', $name
765
            );
766
        }
767
    }
768
769
    return $cache[$name][$skip_group];
770
}
771
772
/**
773
 * callback encodes the matches
774
 *
775
 * @param array $matches first complete match, next matching subpatterms
776
 * @return string
777
 */
778
function auth_nameencode_callback($matches) {
779
    return '%'.dechex(ord(substr($matches[1],-1)));
780
}
781
782
/**
783
 * Create a pronouncable password
784
 *
785
 * The $foruser variable might be used by plugins to run additional password
786
 * policy checks, but is not used by the default implementation
787
 *
788
 * @author   Andreas Gohr <[email protected]>
789
 * @link     http://www.phpbuilder.com/annotate/message.php3?id=1014451
790
 * @triggers AUTH_PASSWORD_GENERATE
791
 *
792
 * @param  string $foruser username for which the password is generated
793
 * @return string  pronouncable password
794
 */
795
function auth_pwgen($foruser = '') {
796
    $data = array(
797
        'password' => '',
798
        'foruser'  => $foruser
799
    );
800
801
    $evt = new Event('AUTH_PASSWORD_GENERATE', $data);
802
    if($evt->advise_before(true)) {
803
        $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
804
        $v = 'aeiou'; //vowels
805
        $a = $c.$v; //both
806
        $s = '!$%&?+*~#-_:.;,'; // specials
807
808
        //use thre syllables...
809
        for($i = 0; $i < 3; $i++) {
810
            $data['password'] .= $c[auth_random(0, strlen($c) - 1)];
811
            $data['password'] .= $v[auth_random(0, strlen($v) - 1)];
812
            $data['password'] .= $a[auth_random(0, strlen($a) - 1)];
813
        }
814
        //... and add a nice number and special
815
        $data['password'] .= $s[auth_random(0, strlen($s) - 1)].auth_random(10, 99);
816
    }
817
    $evt->advise_after();
818
819
    return $data['password'];
820
}
821
822
/**
823
 * Sends a password to the given user
824
 *
825
 * @author  Andreas Gohr <[email protected]>
826
 *
827
 * @param string $user Login name of the user
828
 * @param string $password The new password in clear text
829
 * @return bool  true on success
830
 */
831
function auth_sendPassword($user, $password) {
832
    global $lang;
833
    /* @var AuthPlugin $auth */
834
    global $auth;
835
    if(!$auth) return false;
836
837
    $user     = $auth->cleanUser($user);
838
    $userinfo = $auth->getUserData($user, $requireGroups = false);
839
840
    if(!$userinfo['mail']) return false;
841
842
    $text = rawLocale('password');
843
    $trep = array(
844
        'FULLNAME' => $userinfo['name'],
845
        'LOGIN'    => $user,
846
        'PASSWORD' => $password
847
    );
848
849
    $mail = new Mailer();
850
    $mail->to($mail->getCleanName($userinfo['name']).' <'.$userinfo['mail'].'>');
851
    $mail->subject($lang['regpwmail']);
852
    $mail->setBody($text, $trep);
853
    return $mail->send();
854
}
855
856
/**
857
 * Register a new user
858
 *
859
 * This registers a new user - Data is read directly from $_POST
860
 *
861
 * @author  Andreas Gohr <[email protected]>
862
 *
863
 * @return bool  true on success, false on any error
864
 */
865
function register() {
866
    global $lang;
867
    global $conf;
868
    /* @var \dokuwiki\Extension\AuthPlugin $auth */
869
    global $auth;
870
    global $INPUT;
871
872
    if(!$INPUT->post->bool('save')) return false;
873
    if(!actionOK('register')) return false;
874
875
    // gather input
876
    $login    = trim($auth->cleanUser($INPUT->post->str('login')));
877
    $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname')));
878
    $email    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email')));
879
    $pass     = $INPUT->post->str('pass');
880
    $passchk  = $INPUT->post->str('passchk');
881
882
    if(empty($login) || empty($fullname) || empty($email)) {
883
        msg($lang['regmissing'], -1);
884
        return false;
885
    }
886
887
    if($conf['autopasswd']) {
888
        $pass = auth_pwgen($login); // automatically generate password
889
    } elseif(empty($pass) || empty($passchk)) {
890
        msg($lang['regmissing'], -1); // complain about missing passwords
891
        return false;
892
    } elseif($pass != $passchk) {
893
        msg($lang['regbadpass'], -1); // complain about misspelled passwords
894
        return false;
895
    }
896
897
    //check mail
898
    if(!mail_isvalid($email)) {
899
        msg($lang['regbadmail'], -1);
900
        return false;
901
    }
902
903
    //okay try to create the user
904
    if(!$auth->triggerUserMod('create', array($login, $pass, $fullname, $email))) {
905
        msg($lang['regfail'], -1);
906
        return false;
907
    }
908
909
    // send notification about the new user
910
    $subscription = new RegistrationSubscriptionSender();
911
    $subscription->sendRegister($login, $fullname, $email);
912
913
    // are we done?
914
    if(!$conf['autopasswd']) {
915
        msg($lang['regsuccess2'], 1);
916
        return true;
917
    }
918
919
    // autogenerated password? then send password to user
920
    if(auth_sendPassword($login, $pass)) {
921
        msg($lang['regsuccess'], 1);
922
        return true;
923
    } else {
924
        msg($lang['regmailfail'], -1);
925
        return false;
926
    }
927
}
928
929
/**
930
 * Update user profile
931
 *
932
 * @author    Christopher Smith <[email protected]>
933
 */
934
function updateprofile() {
935
    global $conf;
936
    global $lang;
937
    /* @var AuthPlugin $auth */
938
    global $auth;
939
    /* @var Input $INPUT */
940
    global $INPUT;
941
942
    if(!$INPUT->post->bool('save')) return false;
943
    if(!checkSecurityToken()) return false;
944
945
    if(!actionOK('profile')) {
946
        msg($lang['profna'], -1);
947
        return false;
948
    }
949
950
    $changes         = array();
951
    $changes['pass'] = $INPUT->post->str('newpass');
952
    $changes['name'] = $INPUT->post->str('fullname');
953
    $changes['mail'] = $INPUT->post->str('email');
954
955
    // check misspelled passwords
956
    if($changes['pass'] != $INPUT->post->str('passchk')) {
957
        msg($lang['regbadpass'], -1);
958
        return false;
959
    }
960
961
    // clean fullname and email
962
    $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
963
    $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
964
965
    // no empty name and email (except the backend doesn't support them)
966
    if((empty($changes['name']) && $auth->canDo('modName')) ||
967
        (empty($changes['mail']) && $auth->canDo('modMail'))
968
    ) {
969
        msg($lang['profnoempty'], -1);
970
        return false;
971
    }
972
    if(!mail_isvalid($changes['mail']) && $auth->canDo('modMail')) {
973
        msg($lang['regbadmail'], -1);
974
        return false;
975
    }
976
977
    $changes = array_filter($changes);
978
979
    // check for unavailable capabilities
980
    if(!$auth->canDo('modName')) unset($changes['name']);
981
    if(!$auth->canDo('modMail')) unset($changes['mail']);
982
    if(!$auth->canDo('modPass')) unset($changes['pass']);
983
984
    // anything to do?
985
    if(!count($changes)) {
986
        msg($lang['profnochange'], -1);
987
        return false;
988
    }
989
990
    if($conf['profileconfirm']) {
991
        if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
992
            msg($lang['badpassconfirm'], -1);
993
            return false;
994
        }
995
    }
996
997
    if(!$auth->triggerUserMod('modify', array($INPUT->server->str('REMOTE_USER'), &$changes))) {
998
        msg($lang['proffail'], -1);
999
        return false;
1000
    }
1001
1002
    if($changes['pass']) {
1003
        // update cookie and session with the changed data
1004
        list( /*user*/, $sticky, /*pass*/) = auth_getCookie();
1005
        $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true));
0 ignored issues
show
It seems like auth_cookiesalt(!$sticky, true) targeting auth_cookiesalt() can also be of type boolean; however, auth_encrypt() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1006
        auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky);
1007
    } else {
1008
        // make sure the session is writable
1009
        @session_start();
1010
        // invalidate session cache
1011
        $_SESSION[DOKU_COOKIE]['auth']['time'] = 0;
1012
        session_write_close();
1013
    }
1014
1015
    return true;
1016
}
1017
1018
/**
1019
 * Delete the current logged-in user
1020
 *
1021
 * @return bool true on success, false on any error
1022
 */
1023
function auth_deleteprofile(){
1024
    global $conf;
1025
    global $lang;
1026
    /* @var \dokuwiki\Extension\AuthPlugin $auth */
1027
    global $auth;
1028
    /* @var Input $INPUT */
1029
    global $INPUT;
1030
1031
    if(!$INPUT->post->bool('delete')) return false;
1032
    if(!checkSecurityToken()) return false;
1033
1034
    // action prevented or auth module disallows
1035
    if(!actionOK('profile_delete') || !$auth->canDo('delUser')) {
1036
        msg($lang['profnodelete'], -1);
1037
        return false;
1038
    }
1039
1040
    if(!$INPUT->post->bool('confirm_delete')){
1041
        msg($lang['profconfdeletemissing'], -1);
1042
        return false;
1043
    }
1044
1045
    if($conf['profileconfirm']) {
1046
        if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1047
            msg($lang['badpassconfirm'], -1);
1048
            return false;
1049
        }
1050
    }
1051
1052
    $deleted = array();
1053
    $deleted[] = $INPUT->server->str('REMOTE_USER');
1054
    if($auth->triggerUserMod('delete', array($deleted))) {
1055
        // force and immediate logout including removing the sticky cookie
1056
        auth_logoff();
1057
        return true;
1058
    }
1059
1060
    return false;
1061
}
1062
1063
/**
1064
 * Send a  new password
1065
 *
1066
 * This function handles both phases of the password reset:
1067
 *
1068
 *   - handling the first request of password reset
1069
 *   - validating the password reset auth token
1070
 *
1071
 * @author Benoit Chesneau <[email protected]>
1072
 * @author Chris Smith <[email protected]>
1073
 * @author Andreas Gohr <[email protected]>
1074
 *
1075
 * @return bool true on success, false on any error
1076
 */
1077
function act_resendpwd() {
1078
    global $lang;
1079
    global $conf;
1080
    /* @var AuthPlugin $auth */
1081
    global $auth;
1082
    /* @var Input $INPUT */
1083
    global $INPUT;
1084
1085
    if(!actionOK('resendpwd')) {
1086
        msg($lang['resendna'], -1);
1087
        return false;
1088
    }
1089
1090
    $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
1091
1092
    if($token) {
1093
        // we're in token phase - get user info from token
1094
1095
        $tfile = $conf['cachedir'].'/'.$token[0].'/'.$token.'.pwauth';
1096
        if(!file_exists($tfile)) {
1097
            msg($lang['resendpwdbadauth'], -1);
1098
            $INPUT->remove('pwauth');
1099
            return false;
1100
        }
1101
        // token is only valid for 3 days
1102
        if((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
1103
            msg($lang['resendpwdbadauth'], -1);
1104
            $INPUT->remove('pwauth');
1105
            @unlink($tfile);
1106
            return false;
1107
        }
1108
1109
        $user     = io_readfile($tfile);
1110
        $userinfo = $auth->getUserData($user, $requireGroups = false);
1111
        if(!$userinfo['mail']) {
1112
            msg($lang['resendpwdnouser'], -1);
1113
            return false;
1114
        }
1115
1116
        if(!$conf['autopasswd']) { // we let the user choose a password
1117
            $pass = $INPUT->str('pass');
1118
1119
            // password given correctly?
1120
            if(!$pass) return false;
1121
            if($pass != $INPUT->str('passchk')) {
1122
                msg($lang['regbadpass'], -1);
1123
                return false;
1124
            }
1125
1126
            // change it
1127
            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
1128
                msg($lang['proffail'], -1);
1129
                return false;
1130
            }
1131
1132
        } else { // autogenerate the password and send by mail
1133
1134
            $pass = auth_pwgen($user);
1135
            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
1136
                msg($lang['proffail'], -1);
1137
                return false;
1138
            }
1139
1140
            if(auth_sendPassword($user, $pass)) {
1141
                msg($lang['resendpwdsuccess'], 1);
1142
            } else {
1143
                msg($lang['regmailfail'], -1);
1144
            }
1145
        }
1146
1147
        @unlink($tfile);
1148
        return true;
1149
1150
    } else {
1151
        // we're in request phase
1152
1153
        if(!$INPUT->post->bool('save')) return false;
1154
1155
        if(!$INPUT->post->str('login')) {
1156
            msg($lang['resendpwdmissing'], -1);
1157
            return false;
1158
        } else {
1159
            $user = trim($auth->cleanUser($INPUT->post->str('login')));
1160
        }
1161
1162
        $userinfo = $auth->getUserData($user, $requireGroups = false);
1163
        if(!$userinfo['mail']) {
1164
            msg($lang['resendpwdnouser'], -1);
1165
            return false;
1166
        }
1167
1168
        // generate auth token
1169
        $token = md5(auth_randombytes(16)); // random secret
1170
        $tfile = $conf['cachedir'].'/'.$token[0].'/'.$token.'.pwauth';
1171
        $url   = wl('', array('do'=> 'resendpwd', 'pwauth'=> $token), true, '&');
1172
1173
        io_saveFile($tfile, $user);
1174
1175
        $text = rawLocale('pwconfirm');
1176
        $trep = array(
1177
            'FULLNAME' => $userinfo['name'],
1178
            'LOGIN'    => $user,
1179
            'CONFIRM'  => $url
1180
        );
1181
1182
        $mail = new Mailer();
1183
        $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>');
1184
        $mail->subject($lang['regpwmail']);
1185
        $mail->setBody($text, $trep);
1186
        if($mail->send()) {
1187
            msg($lang['resendpwdconfirm'], 1);
1188
        } else {
1189
            msg($lang['regmailfail'], -1);
1190
        }
1191
        return true;
1192
    }
1193
    // never reached
1194
}
1195
1196
/**
1197
 * Encrypts a password using the given method and salt
1198
 *
1199
 * If the selected method needs a salt and none was given, a random one
1200
 * is chosen.
1201
 *
1202
 * @author  Andreas Gohr <[email protected]>
1203
 *
1204
 * @param string $clear The clear text password
1205
 * @param string $method The hashing method
1206
 * @param string $salt A salt, null for random
1207
 * @return  string  The crypted password
1208
 */
1209
function auth_cryptPassword($clear, $method = '', $salt = null) {
1210
    global $conf;
1211
    if(empty($method)) $method = $conf['passcrypt'];
1212
1213
    $pass = new PassHash();
1214
    $call = 'hash_'.$method;
1215
1216
    if(!method_exists($pass, $call)) {
1217
        msg("Unsupported crypt method $method", -1);
1218
        return false;
1219
    }
1220
1221
    return $pass->$call($clear, $salt);
1222
}
1223
1224
/**
1225
 * Verifies a cleartext password against a crypted hash
1226
 *
1227
 * @author Andreas Gohr <[email protected]>
1228
 *
1229
 * @param  string $clear The clear text password
1230
 * @param  string $crypt The hash to compare with
1231
 * @return bool true if both match
1232
 */
1233
function auth_verifyPassword($clear, $crypt) {
1234
    $pass = new PassHash();
1235
    return $pass->verify_hash($clear, $crypt);
1236
}
1237
1238
/**
1239
 * Set the authentication cookie and add user identification data to the session
1240
 *
1241
 * @param string  $user       username
1242
 * @param string  $pass       encrypted password
1243
 * @param bool    $sticky     whether or not the cookie will last beyond the session
1244
 * @return bool
1245
 */
1246
function auth_setCookie($user, $pass, $sticky) {
1247
    global $conf;
1248
    /* @var AuthPlugin $auth */
1249
    global $auth;
1250
    global $USERINFO;
1251
1252
    if(!$auth) return false;
1253
    $USERINFO = $auth->getUserData($user);
1254
1255
    // set cookie
1256
    $cookie    = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode($pass);
1257
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1258
    $time      = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
1259
    setcookie(DOKU_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
1260
1261
    // set session
1262
    $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
1263
    $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
1264
    $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
1265
    $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
1266
    $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
1267
1268
    return true;
1269
}
1270
1271
/**
1272
 * Returns the user, (encrypted) password and sticky bit from cookie
1273
 *
1274
 * @returns array
1275
 */
1276
function auth_getCookie() {
1277
    if(!isset($_COOKIE[DOKU_COOKIE])) {
1278
        return array(null, null, null);
1279
    }
1280
    list($user, $sticky, $pass) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
1281
    $sticky = (bool) $sticky;
1282
    $pass   = base64_decode($pass);
1283
    $user   = base64_decode($user);
1284
    return array($user, $sticky, $pass);
1285
}
1286
1287
//Setup VIM: ex: et ts=2 :
1288