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/Controller/MetaController.php (4 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Controller;
6
7
use DateTime;
8
use Doctrine\DBAL\Connection;
9
use Doctrine\Persistence\ManagerRegistry;
10
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
11
use Symfony\Component\HttpFoundation\JsonResponse;
12
use Symfony\Component\HttpFoundation\Request;
13
use Symfony\Component\HttpFoundation\Response;
14
use Symfony\Component\Routing\Annotation\Route;
15
16
/**
17
 * This controller serves everything for the Meta tool.
18
 */
19
class MetaController extends XtoolsController
20
{
21
    /**
22
     * @inheritDoc
23
     * @codeCoverageIgnore
24
     */
25
    public function getIndexRoute(): string
26
    {
27
        return 'Meta';
28
    }
29
30
    /**
31
     * Display the form.
32
     * @Route("/meta", name="meta")
33
     * @Route("/meta", name="Meta")
34
     * @Route("/meta/index.php", name="MetaIndexPhp")
35
     * @return Response
36
     */
37
    public function indexAction(): Response
38
    {
39
        if (isset($this->params['start']) && isset($this->params['end'])) {
40
            return $this->redirectToRoute('MetaResult', $this->params);
41
        }
42
43
        return $this->render('meta/index.html.twig', [
44
            'xtPage' => 'Meta',
45
            'xtPageTitle' => 'tool-meta',
46
            'xtSubtitle' => 'tool-meta-desc',
47
        ]);
48
    }
49
50
    /**
51
     * Display the results.
52
     * @Route(
53
     *     "/meta/{start}/{end}/{legacy}",
54
     *     name="MetaResult",
55
     *     requirements={
56
     *         "start"="\d{4}-\d{2}-\d{2}",
57
     *         "end"="\d{4}-\d{2}-\d{2}",
58
     *     },
59
     * )
60
     * @param ManagerRegistry $managerRegistry
61
     * @param bool $legacy Non-blank value indicates to show stats for legacy XTools
62
     * @return Response
63
     * @codeCoverageIgnore
64
     */
65
    public function resultAction(ManagerRegistry $managerRegistry, bool $legacy = false): Response
66
    {
67
        $db = $legacy ? 'toolsdb' : 'default';
68
        $table = $legacy ? 's51187__metadata.xtools_timeline' : 'usage_timeline';
69
        $client = $managerRegistry->getConnection($db);
70
71
        $toolUsage = $this->getToolUsageStats($client, $table);
72
        $apiUsage = $this->getApiUsageStats($client);
73
74
        return $this->render('meta/result.html.twig', [
75
            'xtPage' => 'Meta',
76
            'start' => $this->start,
77
            'end' => $this->end,
78
            'toolUsage' => $toolUsage,
79
            'apiUsage' => $apiUsage,
80
        ]);
81
    }
82
83
    /**
84
     * Get usage statistics of the core tools.
85
     * @param object $client
86
     * @param string $table Table to query.
87
     * @return array
88
     * @codeCoverageIgnore
89
     */
90
    private function getToolUsageStats(object $client, string $table): array
91
    {
92
        $start = date('Y-m-d', $this->start);
0 ignored issues
show
It seems like $this->start can also be of type boolean; however, parameter $timestamp of date() does only seem to accept integer|null, 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

92
        $start = date('Y-m-d', /** @scrutinizer ignore-type */ $this->start);
Loading history...
93
        $end = date('Y-m-d', $this->end);
94
        $data = $client->executeQuery("SELECT * FROM $table WHERE date >= :start AND date <= :end", [
95
            'start' => $start,
96
            'end' => $end,
97
        ])->fetchAllAssociative();
98
99
        // Create array of totals, along with formatted timeline data as needed by Chart.js
100
        $totals = [];
101
        $dateLabels = [];
102
        $timeline = [];
103
        $startObj = new DateTime($start);
104
        $endObj = new DateTime($end);
105
        $numDays = (int) $endObj->diff($startObj)->format("%a");
106
        $grandSum = 0;
107
108
        // Generate array of date labels
109
        for ($dateObj = new DateTime($start); $dateObj <= $endObj; $dateObj->modify('+1 day')) {
110
            $dateLabels[] = $dateObj->format('Y-m-d');
111
        }
112
113
        foreach ($data as $entry) {
114
            if (!isset($totals[$entry['tool']])) {
115
                $totals[$entry['tool']] = (int) $entry['count'];
116
117
                // Create arrays for each tool, filled with zeros for each date in the timeline
118
                $timeline[$entry['tool']] = array_fill(0, $numDays, 0);
119
            } else {
120
                $totals[$entry['tool']] += (int) $entry['count'];
121
            }
122
123
            $date = new DateTime($entry['date']);
124
            $dateIndex = (int) $date->diff($startObj)->format("%a");
125
            $timeline[$entry['tool']][$dateIndex] = (int) $entry['count'];
126
127
            $grandSum += $entry['count'];
128
        }
129
        arsort($totals);
130
131
        return [
132
            'totals' => $totals,
133
            'grandSum' => $grandSum,
134
            'dateLabels' => $dateLabels,
135
            'timeline' => $timeline,
136
        ];
137
    }
138
139
    /**
140
     * Get usage statistics of the API.
141
     * @param object $client
142
     * @return array
143
     * @codeCoverageIgnore
144
     */
145
    private function getApiUsageStats(object $client): array
146
    {
147
        $start = date('Y-m-d', $this->start);
0 ignored issues
show
It seems like $this->start can also be of type boolean; however, parameter $timestamp of date() does only seem to accept integer|null, 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

147
        $start = date('Y-m-d', /** @scrutinizer ignore-type */ $this->start);
Loading history...
148
        $end = date('Y-m-d', $this->end);
149
        $data = $client->executeQuery("SELECT * FROM usage_api_timeline WHERE date >= :start AND date <= :end", [
150
            'start' => $start,
151
            'end' => $end,
152
        ])->fetchAllAssociative();
153
154
        // Create array of totals, along with formatted timeline data as needed by Chart.js
155
        $totals = [];
156
        $dateLabels = [];
157
        $timeline = [];
158
        $startObj = new DateTime($start);
159
        $endObj = new DateTime($end);
160
        $numDays = (int) $endObj->diff($startObj)->format("%a");
161
        $grandSum = 0;
162
163
        // Generate array of date labels
164
        for ($dateObj = new DateTime($start); $dateObj <= $endObj; $dateObj->modify('+1 day')) {
165
            $dateLabels[] = $dateObj->format('Y-m-d');
166
        }
167
168
        foreach ($data as $entry) {
169
            if (!isset($totals[$entry['endpoint']])) {
170
                $totals[$entry['endpoint']] = (int) $entry['count'];
171
172
                // Create arrays for each endpoint, filled with zeros for each date in the timeline
173
                $timeline[$entry['endpoint']] = array_fill(0, $numDays, 0);
174
            } else {
175
                $totals[$entry['endpoint']] += (int) $entry['count'];
176
            }
177
178
            $date = new DateTime($entry['date']);
179
            $dateIndex = (int) $date->diff($startObj)->format("%a");
180
            $timeline[$entry['endpoint']][$dateIndex] = (int) $entry['count'];
181
182
            $grandSum += $entry['count'];
183
        }
184
        arsort($totals);
185
186
        return [
187
            'totals' => $totals,
188
            'grandSum' => $grandSum,
189
            'dateLabels' => $dateLabels,
190
            'timeline' => $timeline,
191
        ];
192
    }
193
194
    /**
195
     * Record usage of a particular XTools tool. This is called automatically
196
     *   in base.html.twig via JavaScript so that it is done asynchronously.
197
     * @Route("/meta/usage/{tool}/{project}/{token}")
198
     * @param Request $request
199
     * @param ParameterBagInterface $parameterBag
200
     * @param ManagerRegistry $managerRegistry
201
     * @param bool $singleWiki
202
     * @param string $tool Internal name of tool.
203
     * @param string $project Project domain such as en.wikipedia.org
204
     * @param string $token Unique token for this request, so we don't have people meddling with these statistics.
205
     * @return JsonResponse
206
     * @codeCoverageIgnore
207
     */
208
    public function recordUsageAction(
209
        Request $request,
210
        ParameterBagInterface $parameterBag,
211
        ManagerRegistry $managerRegistry,
212
        bool $singleWiki,
213
        string $tool,
214
        string $project,
215
        string $token
216
    ): Response {
217
        $response = new JsonResponse();
218
219
        // Validate method and token.
220
        if ('PUT' !== $request->getMethod() || !$this->isCsrfTokenValid('intention', $token)) {
221
            $response->setStatusCode(Response::HTTP_FORBIDDEN);
222
            $response->setContent(json_encode([
223
                'error' => 'This endpoint is for internal use only.',
224
            ]));
225
            return $response;
226
        }
227
228
        // Don't update counts for tools that aren't enabled
229
        $configKey = 'enable.'.ucfirst($tool);
230
        if (!$parameterBag->has($configKey) || !$parameterBag->get($configKey)) {
231
            $response->setStatusCode(Response::HTTP_FORBIDDEN);
232
            $response->setContent(json_encode([
233
                'error' => 'This tool is disabled',
234
            ]));
235
            return $response;
236
        }
237
238
        /** @var Connection $conn */
239
        $conn = $managerRegistry->getConnection('default');
240
        $date =  date('Y-m-d');
241
242
        // Tool name needs to be lowercase.
243
        $tool = strtolower($tool);
244
245
        $sql = "INSERT INTO usage_timeline
246
                VALUES(NULL, :date, :tool, 1)
247
                ON DUPLICATE KEY UPDATE `count` = `count` + 1";
248
        $conn->executeStatement($sql, [
0 ignored issues
show
Security SQL Injection introduced by
array('date' => $date, 'tool' => $tool) 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. Enters via controller action parameter $tool
    in src/Controller/MetaController.php on line 213
  2. Data is passed through strtolower(), and strtolower($tool) is assigned to $tool
    in src/Controller/MetaController.php on line 243

Used in sql context

  1. Connection::executeStatement() is called
    in src/Controller/MetaController.php on line 243
  2. Enters via parameter $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1510
  3. Data is passed through expandListParameters()
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1521
  4. Doctrine\DBAL\SQLParserUtils::expandListParameters($sql, $params, $types) is assigned to $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1521
  5. DB2Statement::execute() is called
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1529
  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...
249
            'date' => $date,
250
            'tool' => $tool,
251
        ]);
252
253
        // Update per-project usage, if applicable
254
        if (!$singleWiki) {
255
            $sql = "INSERT INTO usage_projects
256
                    VALUES(NULL, :tool, :project, 1)
257
                    ON DUPLICATE KEY UPDATE `count` = `count` + 1";
258
            $conn->executeStatement($sql, [
0 ignored issues
show
Security SQL Injection introduced by
array('tool' => $tool, 'project' => $project) can contain request data and is used in sql context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Enters via controller action parameter $project in src/Controller/MetaController.php on line 214
  1. Enters via controller action parameter $project
    in src/Controller/MetaController.php on line 214
  2. Path: Enters via controller action parameter $tool in src/Controller/MetaController.php on line 213
  1. Enters via controller action parameter $tool
    in src/Controller/MetaController.php on line 213
  2. Data is passed through strtolower(), and strtolower($tool) is assigned to $tool
    in src/Controller/MetaController.php on line 243

Used in sql context

  1. Connection::executeStatement() is called
    in src/Controller/MetaController.php on line 253
  2. Enters via parameter $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1510
  3. Data is passed through expandListParameters()
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1521
  4. Doctrine\DBAL\SQLParserUtils::expandListParameters($sql, $params, $types) is assigned to $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1521
  5. DB2Statement::execute() is called
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1529
  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...
259
                'tool' => $tool,
260
                'project' => $project,
261
            ]);
262
        }
263
264
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
265
        $response->setContent(json_encode([]));
266
        return $response;
267
    }
268
}
269