CVE-2020-14971 (published 2020-06-23T14:15Z)

Pi-hole through 5.0 allows code injection in piholedhcp (the Static DHCP Leases section) by modifying Teleporter backup files and then restoring them. This occurs in settings.php. To exploit this, an attacker would request a backup of limited files via teleporter.php. These are placed into a .tar.gz archive. The attacker then modifies the host parameter in dnsmasq.d files, and then compresses and uploads these files again.

Severity: MEDIUM
CVSS v2 base score: 4.6 (AV:L/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 7.8, HIGH (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-862
cwe_name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are not applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/pi-hole/AdminLTE
commit_details: repo_url: https://github.com/pi-hole/AdminLTE
author: Adam Warner
author_date: 2020-06-13 18:50:36+01:00
msg: make use of utils.escapeHtml on the JS side of things, and html_entity_decode/htmlentities in PHP

Signed-off-by: Adam Warner <me@adamwarner.co.uk>
repository: repo_url: https://github.com/pi-hole/AdminLTE
repo_name: pi-hole/web
description: Pi-hole Dashboard for stats and more
repo_language: PHP
owner: pi-hole
file_changes: filename: dns_records.php
change_type: ModificationType.MODIFY
diff: @@ -89,6 +89,7 @@
     </div>
 </div>
 
+<script src="scripts/pi-hole/js/utils.js"></script>
 <script src="scripts/pi-hole/js/ip-address-sorting.js"></script>
 <script src="scripts/pi-hole/js/customdns.js"></script>
 

programming_language: HTML
filename: customdns.js
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,8 @@
  *  This file is copyright under the latest version of the EUPL.
  *  Please see LICENSE file for your rights under this license. */
 
+/* global utils:false */
+
 var table;
 var token = $("#token").text();
 
@@ -76,8 +78,8 @@ $(function () {
 });
 
 function addCustomDNS() {
-  var ip = $("#ip").val();
-  var domain = $("#domain").val();
+  var ip = utils.escapeHtml($("#ip").val());
+  var domain = utils.escapeHtml($("#domain").val());
 
   showAlert("info");
   $.ajax({

programming_language: JavaScript
filename: groups-adlists.js
change_type: ModificationType.MODIFY
diff: @@ -212,8 +212,8 @@ function initTable() {
 }
 
 function addAdlist() {
-  var address = $("#new_address").val();
-  var comment = $("#new_comment").val();
+  var address = utils.escapeHtml($("#new_address").val());
+  var comment = utils.escapeHtml($("#new_comment").val());
 
   utils.disableAll();
   utils.showAlert("info", "", "Adding adlist...", address);
@@ -258,9 +258,9 @@ function editAdlist() {
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
   var status = tr.find("#status_" + id).is(":checked") ? 1 : 0;
-  var comment = tr.find("#comment_" + id).val();
+  var comment = utils.escapeHtml(tr.find("#comment_" + id).val());
   var groups = tr.find("#multiselect_" + id).val();
-  var address = tr.find("#address_" + id).text();
+  var address = utils.escapeHtml(tr.find("#address_" + id).text());
 
   var done = "edited";
   var notDone = "editing";
@@ -338,7 +338,7 @@ function editAdlist() {
 function deleteAdlist() {
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
-  var address = tr.find("#address_" + id).text();
+  var address = utils.escapeHtml(tr.find("#address_" + id).text());
 
   utils.disableAll();
   utils.showAlert("info", "", "Deleting adlist...", address);

programming_language: JavaScript
filename: groups-clients.js
change_type: ModificationType.MODIFY
diff: @@ -246,9 +246,9 @@ function initTable() {
 
 function addClient() {
   var ip = $("#select").val();
-  var comment = $("#new_comment").val();
+  var comment = utils.escapeHtml($("#new_comment").val());
   if (ip === "custom") {
-    ip = $("#ip-custom").val().trim();
+    ip = utils.escapeHtml($("#ip-custom").val().trim());
   }
 
   utils.disableAll();
@@ -303,9 +303,9 @@ function editClient() {
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
   var groups = tr.find("#multiselect_" + id).val();
-  var ip = tr.find("#ip_" + id).text();
-  var name = tr.find("#name_" + id).text();
-  var comment = tr.find("#comment_" + id).val();
+  var ip = utils.escapeHtml(tr.find("#ip_" + id).text());
+  var name = utils.escapeHtml(tr.find("#name_" + id).text());
+  var comment = utils.escapeHtml(tr.find("#comment_" + id).val());
 
   var done = "edited";
   var notDone = "editing";
@@ -370,7 +370,7 @@ function deleteClient() {
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
   var ip = tr.find("#ip_" + id).text();
-  var name = tr.find("#name_" + id).text();
+  var name = utils.escapeHtml(tr.find("#name_" + id).text());
 
   if (name.length > 0) {
     ip += " (" + name + ")";

programming_language: JavaScript
filename: groups-domains.js
change_type: ModificationType.MODIFY
diff: @@ -318,8 +318,8 @@ function addDomain() {
     commentEl = $("#new_regex_comment");
   }
 
-  var domain = domainEl.val();
-  var comment = commentEl.val();
+  var domain = utils.escapeHtml(domainEl.val());
+  var comment = utils.escapeHtml(commentEl.val());
 
   utils.disableAll();
   utils.showAlert("info", "", "Adding " + domainRegex + "...", domain);
@@ -385,10 +385,10 @@ function editDomain() {
   var elem = $(this).attr("id");
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
-  var domain = tr.find("#domain_" + id).text();
+  var domain = utils.escapeHtml(tr.find("#domain_" + id).text());
   var type = tr.find("#type_" + id).val();
   var status = tr.find("#status_" + id).is(":checked") ? 1 : 0;
-  var comment = tr.find("#comment_" + id).val();
+  var comment = utils.escapeHtml(tr.find("#comment_" + id).val());
 
   // Show group assignment field only if in full domain management mode
   // if not included, just use the row data.
@@ -485,7 +485,7 @@ function editDomain() {
 function deleteDomain() {
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
-  var domain = tr.find("#domain_" + id).text();
+  var domain = utils.escapeHtml(tr.find("#domain_" + id).text());
   var type = tr.find("#type_" + id).val();
 
   var domainRegex;

programming_language: JavaScript
filename: groups.js
change_type: ModificationType.MODIFY
diff: @@ -127,8 +127,8 @@ $(function () {
 });
 
 function addGroup() {
-  var name = $("#new_name").val();
-  var desc = $("#new_desc").val();
+  var name = utils.escapeHtml($("#new_name").val());
+  var desc = utils.escapeHtml($("#new_desc").val());
 
   utils.disableAll();
   utils.showAlert("info", "", "Adding group...", name);
@@ -166,9 +166,9 @@ function editGroup() {
   var elem = $(this).attr("id");
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
-  var name = tr.find("#name_" + id).val();
+  var name = utils.escapeHtml(tr.find("#name_" + id).val());
   var status = tr.find("#status_" + id).is(":checked") ? 1 : 0;
-  var desc = tr.find("#desc_" + id).val();
+  var desc = utils.escapeHtml(tr.find("#desc_" + id).val());
 
   var done = "edited";
   var notDone = "editing";
@@ -239,7 +239,7 @@ function editGroup() {
 function deleteGroup() {
   var tr = $(this).closest("tr");
   var id = tr.attr("data-id");
-  var name = tr.find("#name_" + id).val();
+  var name = utils.escapeHtml(tr.find("#name_" + id).val());
 
   utils.disableAll();
   utils.showAlert("info", "", "Deleting group...", name);

programming_language: JavaScript
filename: database.php
change_type: ModificationType.MODIFY
diff: @@ -161,9 +161,9 @@ function add_to_table($db, $table, $domains, $comment=null, $wildcardstyle=false
 		if($wildcardstyle)
 			$domain = "(\\.|^)".str_replace(".","\\.",$domain)."$";
 
-		$stmt->bindValue(":$field", $domain, SQLITE3_TEXT);
+		$stmt->bindValue(":$field", htmlentities($domain), SQLITE3_TEXT);
 		if($bindcomment) {
-			$stmt->bindValue(":comment", $comment, SQLITE3_TEXT);
+			$stmt->bindValue(":comment", htmlentities($comment), SQLITE3_TEXT);
 		}
 
 		if($stmt->execute() && $stmt->reset())

programming_language: PHP
filename: func.php
change_type: ModificationType.MODIFY
diff: @@ -214,31 +214,34 @@ function deleteCustomDNSEntry()
 
 function deleteAllCustomDNSEntries()
 {
-    $handle = fopen($customDNSFile, "r");
-    if ($handle)
+    if (isset($customDNSFile))
     {
-        try
+        $handle = fopen($customDNSFile, "r");
+        if ($handle)
         {
-            while (($line = fgets($handle)) !== false) {
-                $line = str_replace("\r","", $line);
-                $line = str_replace("\n","", $line);
-                $explodedLine = explode (" ", $line);
+            try
+            {
+                while (($line = fgets($handle)) !== false) {
+                    $line = str_replace("\r","", $line);
+                    $line = str_replace("\n","", $line);
+                    $explodedLine = explode (" ", $line);
 
-                if (count($explodedLine) != 2)
-                    continue;
+                    if (count($explodedLine) != 2)
+                        continue;
 
-                $ip = $explodedLine[0];
-                $domain = $explodedLine[1];
+                    $ip = $explodedLine[0];
+                    $domain = $explodedLine[1];
 
-                pihole_execute("-a removecustomdns ".$ip." ".$domain);
+                    pihole_execute("-a removecustomdns ".$ip." ".$domain);
+                }
+            }
+            catch (\Exception $ex)
+            {
+                return errorJsonResponse($ex->getMessage());
             }
-        }
-        catch (\Exception $ex)
-        {
-            return errorJsonResponse($ex->getMessage());
-        }
 
-        fclose($handle);
+            fclose($handle);
+        }
     }
 
     return successJsonResponse();

programming_language: PHP
filename: groups.php
change_type: ModificationType.MODIFY
diff: @@ -58,7 +58,8 @@ if ($_POST['action'] == 'get_groups') {
 } elseif ($_POST['action'] == 'add_group') {
     // Add new group
     try {
-        $names = str_getcsv(trim($_POST['name']), ' ');
+        $input = html_entity_decode(trim($_POST['name']));
+        $names = str_getcsv($input, ' ');
         $total = count($names);
         $added = 0;
         $stmt = $db->prepare('INSERT INTO "group" (name,description) VALUES (:name,:desc)');
@@ -96,6 +97,9 @@ if ($_POST['action'] == 'get_groups') {
 } elseif ($_POST['action'] == 'edit_group') {
     // Edit group identified by ID
     try {
+        $name = html_entity_decode($_POST['name']);
+        $desc = html_entity_decode($_POST['desc']);
+
         $stmt = $db->prepare('UPDATE "group" SET enabled=:enabled, name=:name, description=:desc WHERE id = :id');
         if (!$stmt) {
             throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
@@ -106,11 +110,10 @@ if ($_POST['action'] == 'get_groups') {
             throw new Exception('While binding enabled: ' . $db->lastErrorMsg());
         }
 
-        if (!$stmt->bindValue(':name', $_POST['name'], SQLITE3_TEXT)) {
+        if (!$stmt->bindValue(':name', $name, SQLITE3_TEXT)) {
             throw new Exception('While binding name: ' . $db->lastErrorMsg());
         }
 
-        $desc = $_POST['desc'];
         if (strlen($desc) === 0) {
             // Store NULL in database for empty descriptions
             $desc = null;
@@ -263,7 +266,7 @@ if ($_POST['action'] == 'get_groups') {
                 throw new Exception('While binding ip: ' . $db->lastErrorMsg());
             }
 
-            $comment = $_POST['comment'];
+            $comment = html_entity_decode($_POST['comment']);
             if (strlen($comment) === 0) {
                     // Store NULL in database for empty comments
                     $comment = null;
@@ -293,7 +296,7 @@ if ($_POST['action'] == 'get_groups') {
             throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
         }
 
-        $comment = $_POST['comment'];
+        $comment = html_entity_decode($_POST['comment']);
         if (strlen($comment) === 0) {
                 // Store NULL in database for empty comments
                 $comment = null;
@@ -453,7 +456,7 @@ if ($_POST['action'] == 'get_groups') {
 } elseif ($_POST['action'] == 'add_domain') {
     // Add new domain
     try {
-        $domains = explode(' ', trim($_POST['domain']));
+        $domains = explode(' ', html_entity_decode(trim($_POST['domain'])));
         $before = intval($db->querySingle("SELECT COUNT(*) FROM domainlist;"));
         $total = count($domains);
         $added = 0;
@@ -474,7 +477,7 @@ if ($_POST['action'] == 'get_groups') {
             throw new Exception('While binding type: ' . $db->lastErrorMsg());
         }
 
-        $comment = $_POST['comment'];
+        $comment = html_entity_decode($_POST['comment']);
         if (strlen($comment) === 0) {
             // Store NULL in database for empty comments
             $comment = null;
@@ -573,7 +576,7 @@ if ($_POST['action'] == 'get_groups') {
             throw new Exception('While binding enabled: ' . $db->lastErrorMsg());
         }
 
-        $comment = $_POST['comment'];
+        $comment = html_entity_decode($_POST['comment']);
         if (strlen($comment) === 0) {
                 // Store NULL in database for empty comments
                 $comment = null;
@@ -742,7 +745,7 @@ if ($_POST['action'] == 'get_groups') {
 } elseif ($_POST['action'] == 'add_adlist') {
     // Add new adlist
     try {
-        $addresses = explode(' ', trim($_POST['address']));
+        $addresses = explode(' ', html_entity_decode(trim($_POST['address'])));
         $total = count($addresses);
         $added = 0;
 
@@ -751,7 +754,7 @@ if ($_POST['action'] == 'get_groups') {
             throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
         }
 
-        $comment = $_POST['comment'];
+        $comment = html_entity_decode($_POST['comment']);
         if (strlen($comment) === 0) {
             // Store NULL in database for empty comments
             $comment = null;
@@ -800,7 +803,7 @@ if ($_POST['action'] == 'get_groups') {
             throw new Exception('While binding enabled: ' . $db->lastErrorMsg());
         }
 
-        $comment = $_POST['comment'];
+        $comment = html_entity_decode($_POST['comment']);
         if (strlen($comment) === 0) {
                 // Store NULL in database for empty comments
                 $comment = null;

programming_language: PHP
filename: teleporter.php
change_type: ModificationType.MODIFY
diff: @@ -173,7 +173,7 @@ function archive_restore_table($file, $table, $flush=false)
 	foreach($contents as $row)
 	{
 		// Limit max length for a domain entry to 253 chars
-		if(strlen($row[$field]) > 253)
+		if(isset($field) && strlen($row[$field]) > 253)
 			continue;
 
 		// Bind properties from JSON data
@@ -196,7 +196,7 @@ function archive_restore_table($file, $table, $flush=false)
 				default:
 					$sqltype = "UNK";
 			}
-			$stmt->bindValue(":".$key, $value, $sqltype);
+			$stmt->bindValue(":".$key, htmlentities($value), $sqltype);
 		}
 
 		if($stmt->execute() && $stmt->reset() && $stmt->clear())

programming_language: PHP
filename: settings.php
change_type: ModificationType.MODIFY
diff: @@ -714,7 +714,7 @@ if (isset($_GET['tab']) && in_array($_GET['tab'], array("sysadmin", "adlists", "
                                                             title="Lease type: IPv<?php echo $lease["type"]; ?><br/>Remaining lease time: <?php echo $lease["TIME"]; ?><br/>DHCP UID: <?php echo $lease["clid"]; ?>">
                                                             <td id="MAC"><?php echo $lease["hwaddr"]; ?></td>
                                                             <td id="IP" data-order="<?php echo bin2hex(inet_pton($lease["IP"])); ?>"><?php echo $lease["IP"]; ?></td>
-                                                            <td id="HOST"><?php echo $lease["host"]; ?></td>
+                                                            <td id="HOST"><?php echo htmlentities($lease["host"]); ?></td>
                                                             <td>
                                                                 <button type="button" id="button" class="btn btn-warning btn-xs" data-static="alert">
                                                                     <span class="fas fas fa-file-import"></span>
@@ -742,7 +742,7 @@ if (isset($_GET['tab']) && in_array($_GET['tab'], array("sysadmin", "adlists", "
                                                         <tr>
                                                             <td><?php echo $lease["hwaddr"]; ?></td>
                                                             <td data-order="<?php echo bin2hex(inet_pton($lease["IP"])); ?>"><?php echo $lease["IP"]; ?></td>
-                                                            <td><?php echo $lease["host"]; ?></td>
+                                                            <td><?php echo htmlentities($lease["host"]); ?></td>
                                                             <td><?php if (strlen($lease["hwaddr"]) > 0) { ?>
                                                                 <button type="submit" class="btn btn-danger btn-xs" name="removestatic"
                                                                         value="<?php echo $lease["hwaddr"]; ?>">

programming_language: PHP