This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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
Bug
introduced
by
![]() |
|||||
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
![]() |
|||||
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
array('date' => $date, 'tool' => $tool) can contain request data and is used in sql context(s) leading to a potential security vulnerability.
Used in sql context
Preventing SQL InjectionThere 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 injectionIn 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;
![]() |
|||||
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
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 pointUsed in sql context
Preventing SQL InjectionThere 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 injectionIn 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;
![]() |
|||||
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 |