1<?php 2 3/** 4 * These tests are designed to test the capacity of pluginauth to handle 5 * correct escaping of colon field delimiters and backslashes in user content. 6 * 7 * (Note that these tests set some Real Names, etc. that are may not be 8 * valid in the broader dokuwiki context, but the tests ensure that 9 * authplain won't get unexpectedly surprised.) 10 * 11 * @group plugin_authplain 12 * @group auth_plugins 13 * @group plugins 14 * @group bundled_plugins 15 */ 16class helper_plugin_authplain_escaping_test extends DokuWikiTest { 17 18 protected $pluginsEnabled = array('authplain'); 19 /** @var auth_plugin_authplain */ 20 protected $auth; 21 22 protected function reloadUsers() { 23 /* auth caches data loaded from file, but recreated object forces reload */ 24 $this->auth = new auth_plugin_authplain(); 25 } 26 27 function setUp() : void { 28 global $config_cascade; 29 parent::setUp(); 30 $name = $config_cascade['plainauth.users']['default']; 31 copy($name, $name.".orig"); 32 $this->reloadUsers(); 33 } 34 35 function tearDown() : void { 36 global $config_cascade; 37 parent::tearDown(); 38 $name = $config_cascade['plainauth.users']['default']; 39 copy($name.".orig", $name); 40 } 41 42 public function testMediawikiPasswordHash() { 43 global $conf; 44 $conf['passcrypt'] = 'mediawiki'; 45 $this->auth->createUser("mwuser", "12345", "Mediawiki User", "me@example.com"); 46 $this->reloadUsers(); 47 $this->assertTrue($this->auth->checkPass("mwuser", "12345")); 48 $mwuser = $this->auth->getUserData("mwuser"); 49 $this->assertStringStartsWith(":B:",$mwuser['pass']); 50 $this->assertEquals("Mediawiki User",$mwuser['name']); 51 } 52 53 public function testNameWithColons() { 54 $name = ":Colon: User:"; 55 $this->auth->createUser("colonuser", "password", $name, "me@example.com"); 56 $this->reloadUsers(); 57 $user = $this->auth->getUserData("colonuser"); 58 $this->assertEquals($name,$user['name']); 59 } 60 61 public function testNameWithBackslashes() { 62 $name = "\\Slash\\ User\\"; 63 $this->auth->createUser("slashuser", "password", $name, "me@example.com"); 64 $this->reloadUsers(); 65 $user = $this->auth->getUserData("slashuser"); 66 $this->assertEquals($name,$user['name']); 67 } 68 69 public function testNameWithHash() { 70 $name = "Hash # User"; 71 $this->auth->createUser("slashuser", "password", $name, "me@example.com"); 72 $this->reloadUsers(); 73 $user = $this->auth->getUserData("slashuser"); 74 $this->assertEquals($name,$user['name']); 75 } 76 77 public function testModifyUser() { 78 global $conf; 79 $conf['passcrypt'] = 'mediawiki'; 80 $user = $this->auth->getUserData("testuser"); 81 $user['name'] = "\\New:Crazy:Name\\"; 82 $user['pass'] = "awesome new password"; 83 $this->auth->modifyUser("testuser", $user); 84 $this->reloadUsers(); 85 86 $saved = $this->auth->getUserData("testuser"); 87 $this->assertEquals($saved['name'], $user['name']); 88 $this->assertTrue($this->auth->checkPass("testuser", $user['pass'])); 89 } 90 91 /** 92 * @see testModifyUserRegexMetacharacter 93 */ 94 public function provideRegexMetacharacterUsers() 95 { 96 // Only the dot actually survives cleanUser() and is thus reachable as 97 // a stored username through the normal user flow. The other 98 // metacharacters are normally stripped during cleaning and cannot 99 // occur that way, but are exercised here directly to ensure 100 // modifyUser() escapes the username robustly on its own. 101 // [attacker username, sibling that an unescaped /^<attacker>:/ matches] 102 return [ 103 ['a.b', 'a1b'], // . matches any single character (reachable via cleanUser) 104 ['xy+', 'xyy'], // + matches the repeated preceding character 105 ['ab?', 'a'], // ? makes the preceding character optional 106 ['c[d', 'c[d'], // [ would make the unescaped pattern an invalid regex 107 ]; 108 } 109 110 /** 111 * Modifying a user whose name contains a regex metacharacter must only 112 * touch that user's own line and never match or destroy other accounts. 113 * 114 * @param string $attacker username containing a regex metacharacter 115 * @param string $sibling account an unescaped pattern would also match 116 * @dataProvider provideRegexMetacharacterUsers 117 */ 118 public function testModifyUserRegexMetacharacter($attacker, $sibling) 119 { 120 $this->auth->createUser($attacker, "password", "Attacker", "a@example.com"); 121 if ($sibling !== $attacker) { 122 $this->auth->createUser($sibling, "password", "Sibling", "s@example.com"); 123 } 124 $this->reloadUsers(); 125 126 // the attacker updates only their own profile 127 $user = $this->auth->getUserData($attacker); 128 $user['name'] = "Renamed"; 129 $this->assertTrue($this->auth->modifyUser($attacker, $user)); 130 $this->reloadUsers(); 131 132 // the attacker's own change took effect 133 $this->assertEquals("Renamed", $this->auth->getUserData($attacker)['name']); 134 135 // the sibling account must survive untouched 136 $saved = $this->auth->getUserData($sibling); 137 $this->assertNotFalse($saved, "sibling account '$sibling' was destroyed"); 138 if ($sibling !== $attacker) { 139 $this->assertEquals("Sibling", $saved['name']); 140 } 141 } 142 143 // really only required for developers to ensure this plugin will 144 // work with systems running on PCRE 6.6 and lower. 145 public function testLineSplit(){ 146 $names = array( 147 'plain', 148 'ut-fठ8', 149 'colon:', 150 'backslash\\', 151 'alltogether\\ठ:' 152 ); 153 $userpass = 'user:password_hash:'; 154 $other_user_data = ':email@address:group1,group2'; 155 156 foreach ($names as $testname) { 157 $escaped = str_replace(array('\\',':'),array('\\\\','\\:'),$testname); // escape : & \ 158 $test_line = $userpass.$escaped.$other_user_data; 159 $result = $this->callInaccessibleMethod($this->auth, 'splitUserData', [$test_line]); 160 161 $this->assertEquals($escaped, $result[2]); 162 } 163 } 164 165 /** 166 * @see testCleaning 167 */ 168 public function provideCleaning() 169 { 170 return [ 171 ['user', 'user'], 172 ['USER', 'user'], 173 [' USER ', 'user'], 174 [' US ER ', 'us_er'], 175 ['http://foo;bar', 'http_foo_bar'], 176 ]; 177 } 178 179 /** 180 * @param string $input 181 * @param string $expected 182 * @dataProvider provideCleaning 183 */ 184 public function testCleaning($input, $expected) 185 { 186 $this->assertEquals($expected, $this->auth->cleanUser($input)); 187 $this->assertEquals($expected, $this->auth->cleanGroup($input)); 188 } 189} 190