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/Repository/CategoryEditsRepository.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Repository;
6
7
use App\Helper\AutomatedEditsHelper;
8
use App\Model\Edit;
9
use App\Model\Project;
10
use App\Model\User;
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\Driver\ResultStatement;
13
use Doctrine\DBAL\ParameterType;
14
use Doctrine\Persistence\ManagerRegistry;
15
use GuzzleHttp\Client;
16
use Psr\Cache\CacheItemPoolInterface;
17
use Psr\Log\LoggerInterface;
18
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
19
use Wikimedia\IPUtils;
20
21
/**
22
 * CategoryEditsRepository is responsible for retrieving data from the database
23
 * about the edits made by a user to pages in a set of given categories.
24
 * @codeCoverageIgnore
25
 */
26
class CategoryEditsRepository extends Repository
27
{
28
    protected AutomatedEditsHelper $autoEditsHelper;
29
    protected EditRepository $editRepo;
30
    protected PageRepository $pageRepo;
31
    protected UserRepository $userRepo;
32
33
    /**
34
     * @param ManagerRegistry $managerRegistry
35
     * @param CacheItemPoolInterface $cache
36
     * @param Client $guzzle
37
     * @param LoggerInterface $logger
38
     * @param ParameterBagInterface $parameterBag
39
     * @param bool $isWMF
40
     * @param int $queryTimeout
41
     * @param AutomatedEditsHelper $autoEditsHelper
42
     * @param EditRepository $editRepo
43
     * @param PageRepository $pageRepo
44
     * @param UserRepository $userRepo
45
     */
46
    public function __construct(
47
        ManagerRegistry $managerRegistry,
48
        CacheItemPoolInterface $cache,
49
        Client $guzzle,
50
        LoggerInterface $logger,
51
        ParameterBagInterface $parameterBag,
52
        bool $isWMF,
53
        int $queryTimeout,
54
        AutomatedEditsHelper $autoEditsHelper,
55
        EditRepository $editRepo,
56
        PageRepository $pageRepo,
57
        UserRepository $userRepo
58
    ) {
59
        $this->autoEditsHelper = $autoEditsHelper;
60
        $this->editRepo = $editRepo;
61
        $this->pageRepo = $pageRepo;
62
        $this->userRepo = $userRepo;
63
        parent::__construct($managerRegistry, $cache, $guzzle, $logger, $parameterBag, $isWMF, $queryTimeout);
64
    }
65
66
    /**
67
     * Get the number of edits this user made to the given categories.
68
     * @param Project $project
69
     * @param User $user
70
     * @param string[] $categories
71
     * @param int|false $start Start date as Unix timestamp.
72
     * @param int|false $end End date as Unix timestamp.
73
     * @return int Result of query, see below.
74
     */
75
    public function countCategoryEdits(
76
        Project $project,
77
        User $user,
78
        array $categories,
79
        $start = false,
80
        $end = false
81
    ): int {
82
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_categoryeditcount');
83
        if ($this->cache->hasItem($cacheKey)) {
84
            return $this->cache->getItem($cacheKey)->get();
85
        }
86
87
        $revisionTable = $project->getTableName('revision');
88
        $categorylinksTable = $project->getTableName('categorylinks');
89
        $revDateConditions = $this->getDateConditions($start, $end, false, 'revs.');
90
        $whereClause = 'revs.rev_actor = ?';
91
        $ipcJoin = '';
92
93
        if ($user->isIpRange()) {
94
            $ipcTable = $project->getTableName('ip_changes');
95
            $ipcJoin = "JOIN $ipcTable ON ipc_rev_id = revs.rev_id";
96
            $whereClause = 'ipc_hex BETWEEN ? AND ?';
97
        }
98
99
        $sql = "SELECT COUNT(DISTINCT revs.rev_id)
100
                FROM $revisionTable revs
101
                $ipcJoin
102
                JOIN $categorylinksTable ON cl_from = rev_page
103
                WHERE $whereClause
104
                    AND cl_to IN (?)
105
                    $revDateConditions";
106
        $result = (int)$this->executeStmt($sql, $project, $user, $categories)->fetchOne();
107
108
        // Cache and return.
109
        return $this->setCache($cacheKey, $result);
110
    }
111
112
    /**
113
     * Get number of edits within each individual category.
114
     * @param Project $project
115
     * @param User $user
116
     * @param array $categories
117
     * @param int|false $start
118
     * @param int|false $end
119
     * @return string[] With categories as keys, counts as values.
120
     */
121
    public function getCategoryCounts(
122
        Project $project,
123
        User $user,
124
        array $categories,
125
        $start = false,
126
        $end = false
127
    ): array {
128
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_categorycounts');
129
        if ($this->cache->hasItem($cacheKey)) {
130
            return $this->cache->getItem($cacheKey)->get();
131
        }
132
133
        $revisionTable = $project->getTableName('revision');
134
        $categorylinksTable = $project->getTableName('categorylinks');
135
        $revDateConditions = $this->getDateConditions($start, $end, false, 'revs.');
136
        $whereClause = 'revs.rev_actor = ?';
137
        $ipcJoin = '';
138
139
        if ($user->isIpRange()) {
140
            $ipcTable = $project->getTableName('ip_changes');
141
            $ipcJoin = "JOIN $ipcTable ON ipc_rev_id = revs.rev_id";
142
            $whereClause = 'ipc_hex BETWEEN ? AND ?';
143
        }
144
145
        $sql = "SELECT cl_to AS cat, COUNT(rev_id) AS edit_count, COUNT(DISTINCT rev_page) AS page_count
146
                FROM $revisionTable revs
147
                $ipcJoin
148
                JOIN $categorylinksTable ON cl_from = rev_page
149
                WHERE $whereClause
150
                    AND cl_to IN (?)
151
                    $revDateConditions
152
                GROUP BY cl_to
153
                ORDER BY edit_count DESC";
154
155
        $counts = [];
156
        $stmt = $this->executeStmt($sql, $project, $user, $categories);
157
        while ($result = $stmt->fetchAssociative()) {
158
            $counts[$result['cat']] = [
159
                'editCount' => (int)$result['edit_count'],
160
                'pageCount' => (int)$result['page_count'],
161
            ];
162
        }
163
164
        // Cache and return.
165
        return $this->setCache($cacheKey, $counts);
166
    }
167
168
    /**
169
     * Get contributions made to the given categories.
170
     * @param Project $project
171
     * @param User $user
172
     * @param string[] $categories
173
     * @param int|false $start Start date as Unix timestamp.
174
     * @param int|false $end End date as Unix timestamp.
175
     * @param false|int $offset Unix timestamp. Used for pagination.
176
     * @return string[] Result of query, with columns 'page_title', 'namespace', 'rev_id', 'timestamp', 'minor',
177
     *   'length', 'length_change', 'comment'
178
     */
179
    public function getCategoryEdits(
180
        Project $project,
181
        User $user,
182
        array $categories,
183
        $start = false,
184
        $end = false,
185
        $offset = false
186
    ): array {
187
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_categoryedits');
188
        if ($this->cache->hasItem($cacheKey)) {
189
            return $this->cache->getItem($cacheKey)->get();
190
        }
191
192
        $pageTable = $project->getTableName('page');
193
        $revisionTable = $project->getTableName('revision');
194
        $commentTable = $project->getTableName('comment');
195
        $categorylinksTable = $project->getTableName('categorylinks');
196
        $revDateConditions = $this->getDateConditions($start, $end, $offset, 'revs.');
197
        $whereClause = 'revs.rev_actor = ?';
198
        $ipcJoin = '';
199
200
        if ($user->isIpRange()) {
201
            $ipcTable = $project->getTableName('ip_changes');
202
            $ipcJoin = "JOIN $ipcTable ON ipc_rev_id = revs.rev_id";
203
            $whereClause = 'ipc_hex BETWEEN ? AND ?';
204
        }
205
206
        $sql = "SELECT page_title, page_namespace AS `namespace`, revs.rev_id AS `rev_id`,
207
                    revs.rev_timestamp AS `timestamp`, revs.rev_minor_edit AS `minor`, revs.rev_len AS `length`,
208
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS `length_change`,
209
                    comment_text AS `comment`
210
                FROM $pageTable
211
                JOIN $revisionTable revs ON page_id = revs.rev_page
212
                $ipcJoin
213
                JOIN $categorylinksTable ON cl_from = rev_page
214
                LEFT JOIN $commentTable comment ON revs.rev_comment_id = comment_id
215
                LEFT JOIN $revisionTable parentrevs ON revs.rev_parent_id = parentrevs.rev_id
216
                WHERE $whereClause
217
                    AND cl_to IN (?)
218
                    $revDateConditions
219
                GROUP BY revs.rev_id
220
                ORDER BY revs.rev_timestamp DESC
221
                LIMIT 50";
222
223
        $result = $this->executeStmt($sql, $project, $user, $categories)->fetchAllAssociative();
224
225
        // Cache and return.
226
        return $this->setCache($cacheKey, $result);
227
    }
228
229
    /**
230
     * Bind dates, username and categories then execute the query.
231
     * @param string $sql
232
     * @param Project $project
233
     * @param User $user
234
     * @param string[] $categories
235
     * @return ResultStatement
236
     */
237
    private function executeStmt(
238
        string $sql,
239
        Project $project,
240
        User $user,
241
        array $categories
242
    ): ResultStatement {
243
        if ($user->isIpRange()) {
244
            [$hexStart, $hexEnd] = IPUtils::parseRange($user->getUsername());
245
            $params = [
246
                $hexStart,
247
                $hexEnd,
248
                $categories,
249
            ];
250
            $types = [
251
                ParameterType::STRING,
252
                ParameterType::STRING,
253
                Connection::PARAM_STR_ARRAY,
254
            ];
255
        } else {
256
            $params = [
257
                $user->getActorId($project),
258
                $categories,
259
            ];
260
            $types = [
261
                ParameterType::STRING,
262
                Connection::PARAM_STR_ARRAY,
263
            ];
264
        }
265
266
        return $this->getProjectsConnection($project)
267
            ->executeQuery($sql, $params, $types);
0 ignored issues
show
Security SQL Injection introduced by
$params can contain request data and is used in sql context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. ParameterBag::get() returns request data
    in vendor/symfony/http-foundation/ParameterBag.php on line 90
  2. $this->attributes->get($key, $this) is assigned to $result
    in vendor/symfony/http-foundation/Request.php on line 711
  3. $result is returned
    in vendor/symfony/http-foundation/Request.php on line 712
  4. Data is passed through explode(), and explode('|', $this->request->get('categories')) is assigned to $categories
    in src/Controller/CategoryEditsController.php on line 109
  5. Data is passed through array_map(), and array_map(function(...) { /* ... */ }, $categories) is assigned to property CategoryEditsController::$categories
    in src/Controller/CategoryEditsController.php on line 117
  6. Read from property CategoryEditsController::$categories, and CategoryEdits::__construct() is called
    in src/Controller/CategoryEditsController.php on line 86
  7. Enters via parameter $categories
    in src/Model/CategoryEdits.php on line 43
  8. Data is passed through array_map(), and array_map(function(...) { /* ... */ }, $categories) is assigned to property CategoryEdits::$categories
    in src/Model/CategoryEdits.php on line 51
  9. Read from property CategoryEdits::$categories, and CategoryEditsRepository::getCategoryCounts() is called
    in src/Model/CategoryEdits.php on line 198
  10. Enters via parameter $categories
    in src/Repository/CategoryEditsRepository.php on line 124
  11. CategoryEditsRepository::executeStmt() is called
    in src/Repository/CategoryEditsRepository.php on line 156
  12. Enters via parameter $categories
    in src/Repository/CategoryEditsRepository.php on line 241
  13. array($hexStart, $hexEnd, $categories) is assigned to $params
    in src/Repository/CategoryEditsRepository.php on line 245

Used in sql context

  1. Connection::executeQuery() is called
    in src/Repository/CategoryEditsRepository.php on line 267
  2. Enters via parameter $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1284
  3. Data is passed through expandListParameters()
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1299
  4. Doctrine\DBAL\SQLParserUtils::expandListParameters($sql, $params, $types) is assigned to $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1299
  5. DB2Statement::execute() is called
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1306
  6. Enters via parameter $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php on line 216
  7. db2_execute() is called
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php on line 238

Preventing SQL Injection

There are two options to prevent SQL injection. Generally, it is recommended to use parameter binding:

$stmt = mysqli_prepare("SELECT * FROM users WHERE name = ?");
$stmt->bind_param("s", $taintedUserName);

An alternative – although generally not recommended – is to escape your data manually:

$mysqli = new mysqli('localhost', 'user', 'pass', 'dbname');

$escaped = $mysqli->real_escape_string($taintedUserName);
$mysqli->query("SELECT * FROM users WHERE name = '".$escaped."'");

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
268
    }
269
270
    /**
271
     * Get Edits given revision rows (JOINed on the page table).
272
     * @param Project $project
273
     * @param User $user
274
     * @param array $revs Each must contain 'page_title' and 'namespace'.
275
     * @return Edit[]
276
     */
277
    public function getEditsFromRevs(Project $project, User $user, array $revs): array
278
    {
279
        return Edit::getEditsFromRevs(
280
            $this->pageRepo,
281
            $this->editRepo,
282
            $this->userRepo,
283
            $project,
284
            $user,
285
            $revs
286
        );
287
    }
288
}
289