Issues (196)

Security Analysis    6 potential vulnerabilities

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

  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.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection (4)
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  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.
  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.
  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.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (1)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  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.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  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.
  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.
  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.
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.

src/Model/User.php (13 issues)

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use App\Repository\UserRepository;
8
use DateTime;
9
use Exception;
10
use Wikimedia\IPUtils;
11
12
/**
13
 * A User is a wiki user who has the same username across all projects in an XTools installation.
14
 */
15
class User extends Model
16
{
17
    /** @var int Maximum queryable range for IPv4. */
18
    public const MAX_IPV4_CIDR = 16;
19
20
    /** @var int Maximum queryable range for IPv6. */
21
    public const MAX_IPV6_CIDR = 32;
22
23
    /** @var string The user's username. */
24
    protected string $username;
25
26
    /** @var int[] Quick cache of edit counts, keyed by project domain. */
27
    protected array $editCounts = [];
28
29
    /**
30
     * Create a new User given a username.
31
     * @param string $username
32
     */
33
    public function __construct(UserRepository $repository, string $username)
34
    {
35
        $this->repository = $repository;
36
        if ('ipr-' === substr($username, 0, 4)) {
37
            $username = substr($username, 4);
38
        }
39
        $this->username = ucfirst(str_replace('_', ' ', trim($username)));
40
41
        // IPv6 address are stored as uppercase in the database.
42
        if ($this->isAnon()) {
43
            $this->username = IPUtils::sanitizeIP($this->username);
44
        }
45
    }
46
47
    /**
48
     * Unique identifier for this User, to be used in cache keys. Use of md5 ensures the cache key does not contain
49
     * reserved characters. You could also use the ID, but that may require an unnecessary DB query.
50
     * @see Repository::getCacheKey()
51
     * @return string
52
     */
53
    public function getCacheKey(): string
54
    {
55
        return md5($this->username);
56
    }
57
58
    /**
59
     * Get the username.
60
     * @return string
61
     */
62
    public function getUsername(): string
63
    {
64
        return $this->username;
65
    }
66
67
    /**
68
     * Get a prettified username for IP addresses. For accounts, just the username is returned.
69
     * @return string
70
     */
71
    public function getPrettyUsername(): string
72
    {
73
        if (!$this->isAnon()) {
74
            return $this->username;
75
        }
76
        return IPUtils::prettifyIP($this->username);
77
    }
78
79
    /**
80
     * Get the username identifier that should be used in routing. This only matters for IP ranges,
81
     * which get prefixed with 'ipr-' to ensure they don't conflict with other routing params (such as namespace ID).
82
     * Always use this method when linking to or generating internal routes, and use it nowhere else.
83
     * @return string
84
     */
85
    public function getUsernameIdent(): string
86
    {
87
        if ($this->isIpRange()) {
88
            return 'ipr-'.$this->username;
89
        }
90
        return $this->username;
91
    }
92
93
    /**
94
     * Is this an IP range?
95
     * @return bool
96
     */
97
    public function isIpRange(): bool
98
    {
99
        return IPUtils::isValidRange($this->username);
100
    }
101
102
    /**
103
     * Is this an IPv6 address or range?
104
     * @return bool
105
     */
106
    public function isIPv6(): bool
107
    {
108
        return IPUtils::isIPv6($this->username);
109
    }
110
111
    /**
112
     * Get the common characters between the start and end address of an IPv4 range.
113
     * This is used when running a LIKE query against actor names.
114
     * @return string[]|null
115
     */
116
    public function getIpSubstringFromCidr(): ?string
117
    {
118
        if (!$this->isIpRange()) {
119
            return null;
120
        }
121
122
        if ($this->isIPv6()) {
123
            // Adapted from https://stackoverflow.com/a/10086404/604142 (CC BY-SA 3.0)
124
            [$firstAddrStr, $prefixLen] = explode('/', $this->username);
125
            $firstAddrBin = inet_pton($firstAddrStr);
126
            $firstAddrUnpacked = unpack('H*', $firstAddrBin);
127
            $firstAddrHex = reset($firstAddrUnpacked);
128
            $range[0] = inet_ntop($firstAddrBin);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$range was never initialized. Although not strictly required by PHP, it is generally a good practice to add $range = array(); before regardless.
Loading history...
129
            $flexBits = 128 - $prefixLen;
130
            $lastAddrHex = $firstAddrHex;
131
132
            $pos = 31;
133
            while ($flexBits > 0) {
134
                $orig = substr($lastAddrHex, $pos, 1);
0 ignored issues
show
It seems like $lastAddrHex can also be of type array; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

134
                $orig = substr(/** @scrutinizer ignore-type */ $lastAddrHex, $pos, 1);
Loading history...
135
                $origVal = hexdec($orig);
136
                $newVal = $origVal | (pow(2, min(4, $flexBits)) - 1);
137
                $new = dechex($newVal);
138
                $lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1);
139
                $flexBits -= 4;
140
                $pos -= 1;
141
            }
142
143
            $lastAddrBin = pack('H*', $lastAddrHex);
144
            $range[1] = inet_ntop($lastAddrBin);
145
        } else {
146
            $cidr = explode('/', $this->username);
147
            $range[0] = long2ip(ip2long($cidr[0]) & -1 << (32 - (int)$cidr[1]));
148
            $range[1] = long2ip(ip2long($range[0]) + pow(2, (32 - (int)$cidr[1])) - 1);
149
        }
150
151
        // Find the leftmost common characters between the two addresses.
152
        $common = '';
153
        $startSplit = str_split(strtoupper($range[0]));
154
        $endSplit = str_split(strtoupper($range[1]));
155
        foreach ($startSplit as $index => $char) {
156
            if ($endSplit[$index] === $char) {
157
                $common .= $char;
158
            } else {
159
                break;
160
            }
161
        }
162
163
        return $common;
164
    }
165
166
    /**
167
     * Is this IP range outside the queryable limits?
168
     * @return bool
169
     */
170
    public function isQueryableRange(): bool
171
    {
172
        if (!$this->isIpRange()) {
173
            return true;
174
        }
175
176
        [, $bits] = IPUtils::parseCIDR($this->username);
177
        $limit = $this->isIPv6() ? self::MAX_IPV6_CIDR : self::MAX_IPV4_CIDR;
178
        return (int)$bits >= $limit;
179
    }
180
181
    /**
182
     * Get the user's ID on the given project.
183
     * @param Project $project
184
     * @return int|null
185
     */
186
    public function getId(Project $project): ?int
187
    {
188
        $ret = $this->repository->getIdAndRegistration(
0 ignored issues
show
The method getIdAndRegistration() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

188
        /** @scrutinizer ignore-call */ 
189
        $ret = $this->repository->getIdAndRegistration(
Loading history...
189
            $project->getDatabaseName(),
190
            $this->getUsername()
191
        );
192
193
        return $ret ? (int)$ret['userId'] : null;
194
    }
195
196
    /**
197
     * Get the user's actor ID on the given project.
198
     * @param Project $project
199
     * @return int
200
     */
201
    public function getActorId(Project $project): int
202
    {
203
        return (int)$this->repository->getActorId(
0 ignored issues
show
The method getActorId() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

203
        return (int)$this->repository->/** @scrutinizer ignore-call */ getActorId(
Loading history...
204
            $project->getDatabaseName(),
205
            $this->getUsername()
206
        );
207
    }
208
209
    /**
210
     * Get the user's registration date on the given project.
211
     * @param Project $project
212
     * @return DateTime|null null if no registration date was found.
213
     */
214
    public function getRegistrationDate(Project $project): ?DateTime
215
    {
216
        $ret = $this->repository->getIdAndRegistration(
217
            $project->getDatabaseName(),
218
            $this->getUsername()
219
        );
220
221
        return null !== $ret['regDate']
222
            ? DateTime::createFromFormat('YmdHis', $ret['regDate'])
223
            : null;
224
    }
225
226
    /**
227
     * Get a user's local user rights on the given Project.
228
     * @param Project $project
229
     * @return string[]
230
     */
231
    public function getUserRights(Project $project): array
232
    {
233
        return $this->repository->getUserRights($project, $this);
0 ignored issues
show
The method getUserRights() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

233
        return $this->repository->/** @scrutinizer ignore-call */ getUserRights($project, $this);
Loading history...
234
    }
235
236
    /**
237
     * Get a list of this user's global rights.
238
     * @param Project|null $project A project to query; if not provided, the default will be used.
239
     * @return string[]
240
     */
241
    public function getGlobalUserRights(?Project $project = null): array
242
    {
243
        return $this->repository->getGlobalUserRights($this->getUsername(), $project);
0 ignored issues
show
The method getGlobalUserRights() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

243
        return $this->repository->/** @scrutinizer ignore-call */ getGlobalUserRights($this->getUsername(), $project);
Loading history...
244
    }
245
246
    /**
247
     * Get the user's (system) edit count.
248
     * @param Project $project
249
     * @return int
250
     */
251
    public function getEditCount(Project $project): int
252
    {
253
        $domain = $project->getDomain();
254
        if (isset($this->editCounts[$domain])) {
255
            return $this->editCounts[$domain];
256
        }
257
258
        $this->editCounts[$domain] = (int)$this->repository->getEditCount(
0 ignored issues
show
The method getEditCount() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

258
        $this->editCounts[$domain] = (int)$this->repository->/** @scrutinizer ignore-call */ getEditCount(
Loading history...
259
            $project->getDatabaseName(),
260
            $this->getUsername()
261
        );
262
263
        return $this->editCounts[$domain];
264
    }
265
266
    /**
267
     * Number of edits which if exceeded, will require the user to log in.
268
     * @return int
269
     */
270
    public function numEditsRequiringLogin(): int
271
    {
272
        return $this->repository->numEditsRequiringLogin();
0 ignored issues
show
The method numEditsRequiringLogin() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

272
        return $this->repository->/** @scrutinizer ignore-call */ numEditsRequiringLogin();
Loading history...
273
    }
274
275
    /**
276
     * Maximum number of edits to process, based on configuration.
277
     * @return int
278
     */
279
    public function maxEdits(): int
280
    {
281
        return $this->repository->maxEdits();
0 ignored issues
show
The method maxEdits() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

281
        return $this->repository->/** @scrutinizer ignore-call */ maxEdits();
Loading history...
282
    }
283
284
    /**
285
     * Does this user exist on the given project?
286
     * @param Project $project
287
     * @return bool
288
     */
289
    public function existsOnProject(Project $project): bool
290
    {
291
        return $this->getId($project) > 0;
292
    }
293
294
    /**
295
     * Does this user exist globally?
296
     * @return bool
297
     */
298
    public function existsGlobally(): bool
299
    {
300
        return $this->repository->existsGlobally($this);
0 ignored issues
show
The method existsGlobally() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

300
        return $this->repository->/** @scrutinizer ignore-call */ existsGlobally($this);
Loading history...
301
    }
302
303
    /**
304
     * Is this user an Administrator on the given project?
305
     * @param Project $project The project.
306
     * @return bool
307
     */
308
    public function isAdmin(Project $project): bool
309
    {
310
        return false !== array_search('sysop', $this->getUserRights($project));
311
    }
312
313
    /**
314
     * Is this user an anonymous user (IP)?
315
     * @return bool
316
     */
317
    public function isAnon(): bool
318
    {
319
        return IPUtils::isIPAddress($this->username);
320
    }
321
322
    /**
323
     * Get the expiry of the current block on the user
324
     * @param Project $project The project.
325
     * @return DateTime|bool Expiry as DateTime, true if indefinite, or false if they are not blocked.
326
     */
327
    public function getBlockExpiry(Project $project)
328
    {
329
        $expiry = $this->repository->getBlockExpiry(
0 ignored issues
show
The method getBlockExpiry() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

329
        /** @scrutinizer ignore-call */ 
330
        $expiry = $this->repository->getBlockExpiry(
Loading history...
330
            $project->getDatabaseName(),
331
            $this->getUsername()
332
        );
333
334
        if ('infinity' === $expiry) {
335
            return true;
336
        } elseif (false === $expiry) {
337
            return false;
338
        } else {
339
            return new DateTime($expiry);
340
        }
341
    }
342
343
    /**
344
     * Is this user currently blocked on the given project?
345
     * @param Project $project The project.
346
     * @return bool
347
     */
348
    public function isBlocked(Project $project): bool
349
    {
350
        return false !== $this->getBlockExpiry($project);
351
    }
352
353
    /**
354
     * Does the user have enough edits that we want to require login?
355
     * @param Project $project
356
     * @return bool
357
     */
358
    public function hasManyEdits(Project $project): bool
359
    {
360
        $editCount = $this->getEditCount($project);
361
        return $editCount > $this->numEditsRequiringLogin();
362
    }
363
364
    /**
365
     * Does the user have more edits than maximum amount allowed for processing?
366
     * @param Project $project
367
     * @return bool
368
     */
369
    public function hasTooManyEdits(Project $project): bool
370
    {
371
        $editCount = $this->getEditCount($project);
372
        return $this->maxEdits() > 0 && $editCount > $this->maxEdits();
373
    }
374
375
    /**
376
     * Get edit count within given timeframe and namespace
377
     * @param Project $project
378
     * @param int|string $namespace Namespace ID or 'all' for all namespaces
379
     * @param int|false $start Start date as Unix timestamp.
380
     * @param int|false $end End date as Unix timestamp.
381
     * @return int
382
     */
383
    public function countEdits(Project $project, $namespace = 'all', $start = false, $end = false): int
384
    {
385
        return $this->repository->countEdits($project, $this, $namespace, $start, $end);
0 ignored issues
show
The method countEdits() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

385
        return $this->repository->/** @scrutinizer ignore-call */ countEdits($project, $this, $namespace, $start, $end);
Loading history...
386
    }
387
388
    /**
389
     * Is this user the same as the current XTools user?
390
     * @return bool
391
     */
392
    public function isCurrentlyLoggedIn(): bool
393
    {
394
        try {
395
            $ident = $this->repository->getXtoolsUserInfo();
0 ignored issues
show
The method getXtoolsUserInfo() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\UserRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

395
            /** @scrutinizer ignore-call */ 
396
            $ident = $this->repository->getXtoolsUserInfo();
Loading history...
396
        } catch (Exception $exception) {
397
            return false;
398
        }
399
        return isset($ident->username) && $ident->username === $this->getUsername();
400
    }
401
}
402