x-tools /
xtools
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
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
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
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;
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
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;
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 |