CVE-2021-46252: Cross-Site Request Forgery (CSRF) (CWE-352)
value: A Cross-Site Request Forgery (CSRF) in RequirementsBypassPage.php of Scratch Wiki scratch-confirmaccount-v3 allows attackers to modify account request requirement bypasses.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 6.5
Published: 2022-02-15T23:15Z
Repository: https://github.com/InternationalScratchWiki/scratch-confirmaccount-v3
Language: PHP
Files changed: src/SpecialConfirmAccounts.php, src/subpages/RequirementsBypassPage.php
Commit date: 2022-01-04 11:09:16 -0600
Commit message: Fix CSRF when adding requirements bypass (#155)


Diff: diff --git a/src/SpecialConfirmAccounts.php b/src/SpecialConfirmAccounts.php
index 7d90671..ead7d0b 100755
--- a/src/SpecialConfirmAccounts.php
+++ b/src/SpecialConfirmAccounts.php
@@ -360,11 +360,11 @@ class SpecialConfirmAccounts extends SpecialPage {
 			$this->handleBlockFormSubmission($request, $output, $session);
 		} else if ($request->getText('unblockSubmit')) {
 			$this->handleUnblockFormSubmission($request, $output, $session);
 		} else if ($request->getText('bypassAddUsername') || $request->getText('bypassRemoveUsername')) { //TODO: refactor to move all the subpages into their own files
 			$bypassPage = new RequirementsBypassPage($this);
-			$bypassPage->handleFormSubmission();
+			$bypassPage->handleFormSubmission($session);
 		}
 	}
 
 	function searchByUsername($username, &$request, &$output) {		
 		$linkRenderer = $this->getLinkRenderer();
@@ -443,11 +443,11 @@ class SpecialConfirmAccounts extends SpecialPage {
 			return $this->handleFormSubmission($request, $output, $session);
 		} else if (strpos($par, wfMessage('scratch-confirmaccount-blocks')->text()) === 0) {
 			return $this->blocksPage($par, $request, $output, $session);
 		} else if (strpos($par, wfMessage('scratch-confirmaccount-requirements-bypasses-url')->text()) === 0) {
 			$bypassPage = new RequirementsBypassPage($this);
-			return $bypassPage->render();
+			return $bypassPage->render($session);
 		} else if ($request->getText('username')) {
 			return $this->searchByUsername($request->getText('username'), $request, $output);
 		} else if (isset(statuses[$par])) {
 			return $this->listRequestsByStatus($par, $output);
 		} else if (ctype_digit($par)) {
diff --git a/src/subpages/RequirementsBypassPage.php b/src/subpages/RequirementsBypassPage.php
index be6332e..de4561c 100644
--- a/src/subpages/RequirementsBypassPage.php
+++ b/src/subpages/RequirementsBypassPage.php
@@ -1,7 +1,10 @@
 <?php
 require_once __DIR__ . '/../database/DatabaseInteractions.php';
+require_once __DIR__ . '/../common.php';
+
+use MediaWiki\Session\Session;
 
 class RequirementsBypassPage {
     private $pageContext;
 
     //the mapping of (form field name => function to call with it)
@@ -12,12 +15,17 @@ class RequirementsBypassPage {
 
     function __construct(SpecialPage $pageContext) {
         $this->pageContext = $pageContext;
     }
 
-    function handleFormSubmission() {
-        $request = $request = $this->pageContext->getRequest();
+    function handleFormSubmission(Session &$session) {
+        $request = $this->pageContext->getRequest();
+        
+        if (isCSRF($session, $request->getText('csrftoken'))) {
+    			$this->pageContext->getOutput()->showErrorPage('error', 'scratch-confirmaccount-csrf');
+    			return;
+    		}
 
         $dbw = getTransactableDatabase('scratch-confirmaccount-bypasses');
 
         foreach (self::requestVariableActionMapping as $fieldKey => $action) {
             if ($request->getText($fieldKey)) {
@@ -25,22 +33,26 @@ class RequirementsBypassPage {
             }
         }
 
         commitTransaction($dbw, 'scratch-confirmaccount-bypasses');
 
-        $this->render();
+        $this->render($session);
     }
 
-    function showAddBypassForm() {
+    function showAddBypassForm(Session &$session) {
         $output = $this->pageContext->getOutput();
         $request = $this->pageContext->getRequest();
         
         $output->addHTML(
             new OOUI\FormLayout([
                 'action' => SpecialPage::getTitleFor('ConfirmAccounts', wfMessage('scratch-confirmaccount-requirements-bypasses-url')->text())->getFullURL(),
                 'method' => 'post',
                 'items' => [
+                    new OOUI\HiddenInputWidget([
+                        'name' => 'csrftoken',
+                        'value' => setCSRFToken($session)
+                    ]),
                     new OOUI\ActionFieldLayout(
                         new OOUI\TextInputWidget( [
                             'name' => 'bypassAddUsername',
                             'required' => true,
                             'value' => $request->getText('username')
@@ -54,11 +66,11 @@ class RequirementsBypassPage {
                 ],
             ])
         );
     }
 
-    function showBypassesList() {
+    function showBypassesList(Session &$session) {
         $output = $this->pageContext->getOutput();
 
         $dbr = getReadOnlyDatabase();
 
         $bypassUsernames = getUsernameBypasses($dbr);
@@ -75,10 +87,11 @@ class RequirementsBypassPage {
 
             $table .= Html::element('td', [], $username);
 
             $table .= Html::openElement('td');
             $table .= Html::openElement('form', ['action' => SpecialPage::getTitleFor('ConfirmAccounts', wfMessage('scratch-confirmaccount-requirements-bypasses-url')->text())->getFullURL(), 'method' => 'post']);
+            $table .= Html::element('input', ['type' => 'hidden', 'name' => 'csrftoken', 'value' => setCSRFToken($session)]);
             $table .= Html::element('input', ['type' => 'hidden', 'name' => 'bypassRemoveUsername', 'value' => $username]);
             $table .= Html::element('input', ['type' => 'submit', 'value' => wfMessage('scratch-confirmaccount-requirements-bypasses-remove')->text()]);
             $table .= Html::closeElement('form');
             $table .= Html::closeElement('td');
 
@@ -88,14 +101,14 @@ class RequirementsBypassPage {
         $table .= Html::closeElement('table');
 
         $output->addHTML($table);
     }
 
-    function render() {
+    function render(Session &$session) {
         $output = $this->pageContext->getOutput();
 
         $output->enableOOUI();
 
-        $this->showAddBypassForm();
-        $this->showBypassesList();
+        $this->showAddBypassForm($session);
+        $this->showBypassesList($session);
     }
 }
\ No newline at end of file

Vulnerable code: 			$bypassPage->handleFormSubmission();
			return $bypassPage->render();
    function handleFormSubmission() {
        $request = $request = $this->pageContext->getRequest();
        $this->render();
    function showAddBypassForm() {
    function showBypassesList() {
    function render() {
        $this->showAddBypassForm();
        $this->showBypassesList();
Fixed code: 			$bypassPage->handleFormSubmission($session);
			return $bypassPage->render($session);
require_once __DIR__ . '/../common.php';

use MediaWiki\Session\Session;
    function handleFormSubmission(Session &$session) {
        $request = $this->pageContext->getRequest();
        
        if (isCSRF($session, $request->getText('csrftoken'))) {
    			$this->pageContext->getOutput()->showErrorPage('error', 'scratch-confirmaccount-csrf');
    			return;
    		}
        $this->render($session);
    function showAddBypassForm(Session &$session) {
                    new OOUI\HiddenInputWidget([
                        'name' => 'csrftoken',
                        'value' => setCSRFToken($session)
                    ]),
    function showBypassesList(Session &$session) {
            $table .= Html::element('input', ['type' => 'hidden', 'name' => 'csrftoken', 'value' => setCSRFToken($session)]);
    function render(Session &$session) {
        $this->showAddBypassForm($session);
        $this->showBypassesList($session);