xref: /dokuwiki/lib/plugins/authplain/_test/escaping.test.php (revision 3ed34089439ec60adc294b4d4b976c5c88cf5be5)
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