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/Helper/AutomatedEditsHelper.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Helper;
6
7
use App\Model\Project;
8
use DateInterval;
9
use MediaWiki\OAuthClient\Client;
10
use Psr\Cache\CacheItemPoolInterface;
11
use Symfony\Component\HttpFoundation\RequestStack;
12
use Symfony\Component\HttpFoundation\Session\SessionInterface;
13
14
/**
15
 * Helper class for fetching semi-automated definitions.
16
 */
17
class AutomatedEditsHelper
18
{
19
    protected CacheItemPoolInterface $cache;
20
    protected SessionInterface $session;
21
22
    /** @var array The list of tools that are considered reverting. */
23
    protected array $revertTools = [];
24
25
    /** @var array The list of tool names and their regexes/tags. */
26
    protected array $tools = [];
27
28
    /**
29
     * AutomatedEditsHelper constructor.
30
     * @param RequestStack $requestStack
31
     * @param CacheItemPoolInterface $cache
32
     */
33
    public function __construct(RequestStack $requestStack, CacheItemPoolInterface $cache)
34
    {
35
        $this->session = $requestStack->getSession();
36
        $this->cache = $cache;
37
    }
38
39
    /**
40
     * Get the tool that matched the given edit summary.
41
     * This only works for tools defined with regular expressions, not tags.
42
     * @param string $summary Edit summary
43
     * @param Project $project
44
     * @return string[]|null Tool entry including key for 'name', or false if nothing was found
45
     */
46
    public function getTool(string $summary, Project $project): ?array
47
    {
48
        foreach ($this->getTools($project) as $tool => $values) {
49
            if (isset($values['regex']) && preg_match('/'.$values['regex'].'/', $summary)) {
50
                return array_merge([
51
                    'name' => $tool,
52
                ], $values);
53
            }
54
        }
55
56
        return null;
57
    }
58
59
    /**
60
     * Was the edit (semi-)automated, based on the edit summary?
61
     * This only works for tools defined with regular expressions, not tags.
62
     * @param string $summary Edit summary
63
     * @param Project $project
64
     * @return bool
65
     */
66
    public function isAutomated(string $summary, Project $project): bool
67
    {
68
        return (bool)$this->getTool($summary, $project);
69
    }
70
71
    /**
72
     * Fetch the config from https://meta.wikimedia.org/wiki/MediaWiki:XTools-AutoEdits.json
73
     * @param bool $useSandbox Use the sandbox version of the config, located at MediaWiki:XTools-AutoEdits.json/sandbox
74
     * @return array
75
     */
76
    public function getConfig(bool $useSandbox = false): array
77
    {
78
        $cacheKey = 'autoedits_config';
79
        if (!$useSandbox && $this->cache->hasItem($cacheKey)) {
80
            return $this->cache->getItem($cacheKey)->get();
81
        }
82
83
        $uri = 'https://meta.wikimedia.org/w/api.php?' . http_build_query([
84
            'action' => 'query',
85
            'prop' => 'revisions',
86
            'rvprop' => 'content',
87
            'rvslots' => 'main',
88
            'format' => 'json',
89
            'formatversion' => 2,
90
            'titles' => 'MediaWiki:XTools-AutoEdits.json' . ($useSandbox ? '/sandbox' : ''),
91
        ]);
92
93
        if ($useSandbox && $this->session->get('logged_in_user')) {
94
            // Request via OAuth to get around server-side caching.
95
            /** @var Client $client */
96
            $client = $this->session->get('oauth_client');
97
            $resp = json_decode($client->makeOAuthCall(
98
                $this->session->get('oauth_access_token'),
99
                $uri
100
            ));
101
        } else {
102
            $resp = json_decode(file_get_contents($uri));
103
        }
104
105
        $ret = json_decode($resp->query->pages[0]->revisions[0]->slots->main->content, true);
106
107
        if (!$useSandbox) {
108
            $cacheItem = $this->cache
109
                ->getItem($cacheKey)
110
                ->set($ret)
111
                ->expiresAfter(new DateInterval('PT20M'));
112
            $this->cache->save($cacheItem);
113
        }
114
115
        return $ret;
116
    }
117
118
    /**
119
     * Get list of automated tools and their associated info for the given project.
120
     * This defaults to the DEFAULT_PROJECT if entries for the given project are not found.
121
     * @param Project $project
122
     * @param bool $useSandbox Whether to use the /sandbox version for testing (also bypasses caching).
123
     * @return array Each tool with the tool name as the key and 'link', 'regex' and/or 'tag' as the subarray keys.
124
     */
125
    public function getTools(Project $project, bool $useSandbox = false): array
126
    {
127
        $projectDomain = $project->getDomain();
128
129
        if (isset($this->tools[$projectDomain])) {
130
            return $this->tools[$projectDomain];
131
        }
132
133
        // Load the semi-automated edit types.
134
        $tools = $this->getConfig($useSandbox);
135
136
        if (isset($tools[$projectDomain])) {
137
            $localRules = $tools[$projectDomain];
138
        } else {
139
            $localRules = [];
140
        }
141
142
        $langRules = $tools[$project->getLang()] ?? [];
143
144
        // Per-wiki rules have priority, followed by language-specific and global.
145
        $globalWithLangRules = $this->mergeRules($tools['global'], $langRules);
146
147
        $this->tools[$projectDomain] = $this->mergeRules(
148
            $globalWithLangRules,
149
            $localRules
150
        );
151
152
        // Once last walk through for some tidying up and validation.
153
        $invalid = [];
154
        array_walk($this->tools[$projectDomain], function (&$data, $tool) use (&$invalid): void {
155
            // Populate the 'label' with the tool name, if a label doesn't already exist.
156
            $data['label'] = $data['label'] ?? $tool;
157
158
            // 'namespaces' should be an array of ints.
159
            $data['namespaces'] = $data['namespaces'] ?? [];
160
            if (isset($data['namespace'])) {
161
                $data['namespaces'][] = $data['namespace'];
162
                unset($data['namespace']);
163
            }
164
165
            // 'tags' should be an array of strings.
166
            $data['tags'] = $data['tags'] ?? [];
167
            if (isset($data['tag'])) {
168
                $data['tags'][] = $data['tag'];
169
                unset($data['tag']);
170
            }
171
172
            // If neither a tag or regex is given, it's invalid.
173
            if (empty($data['tags']) && empty($data['regex'])) {
174
                $invalid[] = $tool;
175
            }
176
        });
177
178
        uksort($this->tools[$projectDomain], 'strcasecmp');
179
180
        if ($invalid) {
181
            $this->tools[$projectDomain]['invalid'] = $invalid;
182
        }
183
184
        return $this->tools[$projectDomain];
185
    }
186
187
    /**
188
     * Merges the given rule sets, giving priority to the local set. Regex is concatenated, not overridden.
189
     * @param string[] $globalRules The global rule set.
190
     * @param string[] $localRules The rule set for the local wiki.
191
     * @return string[] Merged rules.
192
     */
193
    private function mergeRules(array $globalRules, array $localRules): array
194
    {
195
        // Initial set, including just the global rules.
196
        $tools = $globalRules;
197
198
        // Loop through local rules and override/merge as necessary.
199
        foreach ($localRules as $tool => $rules) {
200
            $newRules = $rules;
201
202
            if (isset($globalRules[$tool])) {
203
                // Order within array_merge is important, so that local rules get priority.
204
                $newRules = array_merge($globalRules[$tool], $rules);
0 ignored issues
show
$globalRules[$tool] of type string is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

204
                $newRules = array_merge(/** @scrutinizer ignore-type */ $globalRules[$tool], $rules);
Loading history...
205
            }
206
207
            // Regex should be merged, not overridden.
208
            if (isset($rules['regex']) && isset($globalRules[$tool]['regex'])) {
209
                $newRules['regex'] = implode('|', [
210
                    $rules['regex'],
211
                    $globalRules[$tool]['regex'],
212
                ]);
213
            }
214
215
            $tools[$tool] = $newRules;
216
        }
217
218
        return $tools;
219
    }
220
221
    /**
222
     * Get only tools that are used to revert edits.
223
     * Revert detection happens only by testing against a regular expression, and not by checking tags.
224
     * @param Project $project
225
     * @return string[][] Each tool with the tool name as the key,
226
     *   and 'link' and 'regex' as the subarray keys.
227
     */
228
    public function getRevertTools(Project $project): array
229
    {
230
        $projectDomain = $project->getDomain();
231
232
        if (isset($this->revertTools[$projectDomain])) {
233
            return $this->revertTools[$projectDomain];
234
        }
235
236
        $revertEntries = array_filter(
237
            $this->getTools($project),
238
            function ($tool) {
239
                return isset($tool['revert']) && isset($tool['regex']);
240
            }
241
        );
242
243
        // If 'revert' is set to `true`, then use 'regex' as the regular expression,
244
        //  otherwise 'revert' is assumed to be the regex string.
245
        $this->revertTools[$projectDomain] = array_map(function ($revertTool) {
246
            return [
247
                'link' => $revertTool['link'],
248
                'regex' => true === $revertTool['revert'] ? $revertTool['regex'] : $revertTool['revert'],
249
            ];
250
        }, $revertEntries);
251
252
        return $this->revertTools[$projectDomain];
253
    }
254
255
    /**
256
     * Was the edit a revert, based on the edit summary?
257
     * This only works for tools defined with regular expressions, not tags.
258
     * @param string|null $summary Edit summary. Can be null for instance for suppressed edits.
259
     * @param Project $project
260
     * @return bool
261
     */
262
    public function isRevert(?string $summary, Project $project): bool
263
    {
264
        foreach (array_values($this->getRevertTools($project)) as $values) {
265
            if (preg_match('/'.$values['regex'].'/', (string)$summary)) {
266
                return true;
267
            }
268
        }
269
270
        return false;
271
    }
272
}
273