CVE-2021-3920: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: grav-plugin-admin is vulnerable to Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2021-11-19T13:15Z
Repository: https://github.com/getgrav/grav-plugin-admin
Language: PHP
Files changed: CHANGELOG.md, classes/plugin/AdminBaseController.php, classes/plugin/AdminController.php, classes/plugin/Controllers/AbstractController.php
Commit date: 2021-11-03 12:42:27 +0200
Commit message: Fixed unescaped messages in JSON responses

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc06bf7a6..88b460c65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
+# v1.10.25
+## mm/dd/2021
+
+3. [](#bugfix)
+   * Fixed unescaped messages in JSON responses
+
 # v1.10.24
 ## 10/26/2021
 
 1. [](#new)
    * Require **Grav 1.7.24**
diff --git a/classes/plugin/AdminBaseController.php b/classes/plugin/AdminBaseController.php
index ebdb45f5c..13e583eec 100644
--- a/classes/plugin/AdminBaseController.php
+++ b/classes/plugin/AdminBaseController.php
@@ -269,11 +269,11 @@ class AdminBaseController
         // Handle bad filenames.
         if (!Utils::checkFilename($filename)) {
             $this->admin->json_response = [
                 'status'  => 'error',
                 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
-                    $filename, 'Bad filename')
+                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
             ];
 
             return false;
         }
 
@@ -289,22 +289,23 @@ class AdminBaseController
         // Do not use self@ outside of pages
         if ($this->view !== 'pages' && in_array($settings->destination, ['@self', 'self@', '@self@'])) {
             $this->admin->json_response = [
                 'status'  => 'error',
                 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null),
-                    $settings->destination)
+                    htmlspecialchars($settings->destination, ENT_QUOTES | ENT_HTML5, 'UTF-8'))
             ];
 
             return false;
         }
 
         // Handle errors and breaks without proceeding further
         if ($upload->file->error !== UPLOAD_ERR_OK) {
             $this->admin->json_response = [
                 'status'  => 'error',
                 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
-                    $filename, $this->upload_errors[$upload->file->error])
+                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
+                    $this->upload_errors[$upload->file->error])
             ];
 
             return false;
         }
 
@@ -338,19 +339,19 @@ class AdminBaseController
             $find   = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type);
 
             if ($isMime) {
                 $match = preg_match('#' . $find . '$#', $mime);
                 if (!$match) {
-                    $errors[] = 'The MIME type "' . $mime . '" for the file "' . $filename . '" is not an accepted.';
+                    $errors[] = htmlspecialchars('The MIME type "' . $mime . '" for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
                 } else {
                     $accepted = true;
                     break;
                 }
             } else {
                 $match = preg_match('#' . $find . '$#', $filename);
                 if (!$match) {
-                    $errors[] = 'The File Extension for the file "' . $filename . '" is not an accepted.';
+                    $errors[] = htmlspecialchars('The File Extension for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
                 } else {
                     $accepted = true;
                     break;
                 }
             }
@@ -377,12 +378,15 @@ class AdminBaseController
 
         Folder::create(dirname($tmp));
         if (!move_uploaded_file($tmp_file, $tmp)) {
             $this->admin->json_response = [
                 'status'  => 'error',
-                'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null), '',
-                    $tmp)
+                'message' => sprintf(
+                    $this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null),
+                    '',
+                    htmlspecialchars($tmp, ENT_QUOTES | ENT_HTML5, 'UTF-8')
+                )
             ];
 
             return false;
         }
 
diff --git a/classes/plugin/AdminController.php b/classes/plugin/AdminController.php
index 05df15e47..fe0b33910 100644
--- a/classes/plugin/AdminController.php
+++ b/classes/plugin/AdminController.php
@@ -286,11 +286,11 @@ class AdminController extends AdminBaseController
         } catch (\Exception $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
             return false;
         }
 
         return true;
     }
@@ -405,11 +405,11 @@ class AdminController extends AdminBaseController
         } catch (\Exception $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
         }
 
         $this->sendJsonResponse($json_response);
     }
 
@@ -488,11 +488,11 @@ class AdminController extends AdminBaseController
         } catch (MalformedXmlException $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
         }
 
         $this->sendJsonResponse($json_response);
     }
 
@@ -538,11 +538,11 @@ class AdminController extends AdminBaseController
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
             $this->admin->json_response = [
                 'status' => 'error',
-                'message' => $this->admin::translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . $e->getMessage()
+                'message' => $this->admin::translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')
             ];
 
             return true;
         }
 
@@ -915,11 +915,11 @@ class AdminController extends AdminBaseController
         } catch (\Exception $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
 
             return false;
         }
 
         return true;
@@ -959,11 +959,11 @@ class AdminController extends AdminBaseController
         } catch (\Exception $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
 
             return false;
         }
 
         $this->admin->json_response = ['status' => 'success', 'dependencies' => $dependencies];
@@ -1002,11 +1002,11 @@ class AdminController extends AdminBaseController
         } catch (\Exception $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
 
             return false;
         }
 
         $result = Gpm::install(array_keys($dependencies), ['theme' => $type === 'themes']);
@@ -1057,11 +1057,11 @@ class AdminController extends AdminBaseController
 
             $msg = $e->getMessage();
             $msg = Utils::contains($msg, '401 Unauthorized') ? "ERROR: License key for this resource is invalid." : $msg;
             $msg = Utils::contains($msg, '404 Not Found') ? "ERROR: Resource not found" : $msg;
 
-            $this->admin->json_response = ['status' => 'error', 'message' => $msg];
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($msg, ENT_QUOTES | ENT_HTML5, 'UTF-8')];
 
             return false;
         }
 
         if ($result) {
@@ -1131,11 +1131,11 @@ class AdminController extends AdminBaseController
         } catch (\Exception $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
 
             $this->sendJsonResponse($json_response, 200);
         }
 
         if ($result) {
@@ -2066,11 +2066,11 @@ class AdminController extends AdminBaseController
         } catch (\Exception $e) {
             /** @var Debugger $debugger */
             $debugger = $this->grav['debugger'];
             $debugger->addException($e);
 
-            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
 
             return false;
         }
 
         return true;
@@ -2223,11 +2223,11 @@ class AdminController extends AdminBaseController
         // Handle bad filenames.
         if (!Utils::checkFilename($filename)) {
             $this->admin->json_response = [
                 'status'  => 'error',
                 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'),
-                    $filename, 'Bad filename')
+                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
             ];
 
             return false;
         }
 
@@ -2451,11 +2451,11 @@ class AdminController extends AdminBaseController
             $result = unlink($targetPath);
 
             if (!$result) {
                 $this->admin->json_response = [
                     'status'  => 'error',
-                    'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
                 ];
 
                 return false;
             }
         }
@@ -2472,11 +2472,11 @@ class AdminController extends AdminBaseController
                 $result = unlink($targetPath);
 
                 if (!$result) {
                     $this->admin->json_response = [
                         'status'  => 'error',
-                        'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename
+                        'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
                     ];
 
                     return false;
                 }
 
@@ -2487,22 +2487,22 @@ class AdminController extends AdminBaseController
         Cache::clearCache('invalidate');
 
         if (!$found) {
             $this->admin->json_response = [
                 'status'  => 'error',
-                'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . $filename
+                'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
             ];
 
             return false;
         }
 
         // DEPRECATED: page
         $this->grav->fireEvent('onAdminAfterDelMedia', new Event(['object' => $page, 'page' => $page, 'media' => $media, 'filename' => $filename]));
 
         $this->admin->json_response = [
             'status'  => 'success',
-            'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename
+            'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
         ];
 
         return true;
     }
 
diff --git a/classes/plugin/Controllers/AbstractController.php b/classes/plugin/Controllers/AbstractController.php
index ff7a8ff9a..4949727c1 100644
--- a/classes/plugin/Controllers/AbstractController.php
+++ b/classes/plugin/Controllers/AbstractController.php
@@ -290,11 +290,11 @@ abstract class AbstractController implements RequestHandlerInterface
 
         $message = $exception->getMessage();
         $response = [
             'code' => $code,
             'status' => 'error',
-            'message' => $message
+            'message' => htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8')
         ];
 
         $accept = $this->getAccept(['application/json', 'text/html']);
 
         if ($accept === 'text/html') {

Vulnerable code:                     $filename, 'Bad filename')
                    $settings->destination)
                    $filename, $this->upload_errors[$upload->file->error])
                    $errors[] = 'The MIME type "' . $mime . '" for the file "' . $filename . '" is not an accepted.';
                    $errors[] = 'The File Extension for the file "' . $filename . '" is not an accepted.';
                'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null), '',
                    $tmp)
            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
            $json_response = ['status' => 'error', 'message' => $e->getMessage()];
            $json_response = ['status' => 'error', 'message' => $e->getMessage()];
                'message' => $this->admin::translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . $e->getMessage()
            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
            $this->admin->json_response = ['status' => 'error', 'message' => $msg];
            $json_response = ['status' => 'error', 'message' => $e->getMessage()];
            $this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
                    $filename, 'Bad filename')
                    'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename
                        'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename
                'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . $filename
            'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename
            'message' => $message
Fixed code: # v1.10.25
## mm/dd/2021

3. [](#bugfix)
   * Fixed unescaped messages in JSON responses

                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
                    htmlspecialchars($settings->destination, ENT_QUOTES | ENT_HTML5, 'UTF-8'))
                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
                    $this->upload_errors[$upload->file->error])
                    $errors[] = htmlspecialchars('The MIME type "' . $mime . '" for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
                    $errors[] = htmlspecialchars('The File Extension for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
                'message' => sprintf(
                    $this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null),
                    '',
                    htmlspecialchars($tmp, ENT_QUOTES | ENT_HTML5, 'UTF-8')
                )
            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
                'message' => $this->admin::translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')
            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($msg, ENT_QUOTES | ENT_HTML5, 'UTF-8')];
            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
                    'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
                        'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
                'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
            'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
            'message' => htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8')