CVE-2020-14971 (PHP, published 2020-06-23T14:15Z)
Description: value: 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 v3 base score: 7.8)
Weakness: CWE-862 \u2014 Missing Authorization
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
Repository: https://github.com/pi-hole/AdminLTE
Files changed:
dns_records.php
scripts/pi-hole/js/customdns.js
scripts/pi-hole/js/groups-adlists.js
scripts/pi-hole/js/groups-clients.js
scripts/pi-hole/js/groups-domains.js
scripts/pi-hole/js/groups.js
scripts/pi-hole/php/database.php
scripts/pi-hole/php/func.php
scripts/pi-hole/php/groups.php
scripts/pi-hole/php/teleporter.php
settings.php
Commit message: 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>


Vulnerable code:
  var ip = $("#ip").val();
  var domain = $("#domain").val();
  var address = $("#new_address").val();
  var comment = $("#new_comment").val();
  var comment = tr.find("#comment_" + id).val();
  var address = tr.find("#address_" + id).text();
  var address = tr.find("#address_" + id).text();
  var comment = $("#new_comment").val();
    ip = $("#ip-custom").val().trim();
  var ip = tr.find("#ip_" + id).text();
  var name = tr.find("#name_" + id).text();
  var comment = tr.find("#comment_" + id).val();
  var name = tr.find("#name_" + id).text();
  var domain = domainEl.val();
  var comment = commentEl.val();
  var domain = tr.find("#domain_" + id).text();
  var comment = tr.find("#comment_" + id).val();
  var domain = tr.find("#domain_" + id).text();
  var name = $("#new_name").val();
  var desc = $("#new_desc").val();
  var name = tr.find("#name_" + id).val();
  var desc = tr.find("#desc_" + id).val();
  var name = tr.find("#name_" + id).val();
		$stmt->bindValue(":$field", $domain, SQLITE3_TEXT);
			$stmt->bindValue(":comment", $comment, SQLITE3_TEXT);
    $handle = fopen($customDNSFile, "r");
    if ($handle)
        try
            while (($line = fgets($handle)) !== false) {
                $line = str_replace("\r","", $line);
                $line = str_replace("\n","", $line);
                $explodedLine = explode (" ", $line);
                if (count($explodedLine) != 2)
                    continue;
                $ip = $explodedLine[0];
                $domain = $explodedLine[1];
                pihole_execute("-a removecustomdns ".$ip." ".$domain);
        }
        catch (\Exception $ex)
        {
            return errorJsonResponse($ex->getMessage());
        }
        fclose($handle);
        $names = str_getcsv(trim($_POST['name']), ' ');
        if (!$stmt->bindValue(':name', $_POST['name'], SQLITE3_TEXT)) {
        $desc = $_POST['desc'];
            $comment = $_POST['comment'];
        $comment = $_POST['comment'];
        $domains = explode(' ', trim($_POST['domain']));
        $comment = $_POST['comment'];
        $comment = $_POST['comment'];
        $addresses = explode(' ', trim($_POST['address']));
        $comment = $_POST['comment'];
        $comment = $_POST['comment'];
		if(strlen($row[$field]) > 253)
			$stmt->bindValue(":".$key, $value, $sqltype);
                                                            <td id="HOST"><?php echo $lease["host"]; ?></td>
                                                            <td><?php echo $lease["host"]; ?></td>

Fixed code:
<script src="scripts/pi-hole/js/utils.js"></script>
/* global utils:false */

  var ip = utils.escapeHtml($("#ip").val());
  var domain = utils.escapeHtml($("#domain").val());
  var address = utils.escapeHtml($("#new_address").val());
  var comment = utils.escapeHtml($("#new_comment").val());
  var comment = utils.escapeHtml(tr.find("#comment_" + id).val());
  var address = utils.escapeHtml(tr.find("#address_" + id).text());
  var address = utils.escapeHtml(tr.find("#address_" + id).text());
  var comment = utils.escapeHtml($("#new_comment").val());
    ip = utils.escapeHtml($("#ip-custom").val().trim());
  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 name = utils.escapeHtml(tr.find("#name_" + id).text());
  var domain = utils.escapeHtml(domainEl.val());
  var comment = utils.escapeHtml(commentEl.val());
  var domain = utils.escapeHtml(tr.find("#domain_" + id).text());
  var comment = utils.escapeHtml(tr.find("#comment_" + id).val());
  var domain = utils.escapeHtml(tr.find("#domain_" + id).text());
  var name = utils.escapeHtml($("#new_name").val());
  var desc = utils.escapeHtml($("#new_desc").val());
  var name = utils.escapeHtml(tr.find("#name_" + id).val());
  var desc = utils.escapeHtml(tr.find("#desc_" + id).val());
  var name = utils.escapeHtml(tr.find("#name_" + id).val());
		$stmt->bindValue(":$field", htmlentities($domain), SQLITE3_TEXT);
			$stmt->bindValue(":comment", htmlentities($comment), SQLITE3_TEXT);
    if (isset($customDNSFile))
        $handle = fopen($customDNSFile, "r");
        if ($handle)
            try
            {
                while (($line = fgets($handle)) !== false) {
                    $line = str_replace("\r","", $line);
                    $line = str_replace("\n","", $line);
                    $explodedLine = explode (" ", $line);
                    if (count($explodedLine) != 2)
                        continue;
                    $ip = $explodedLine[0];
                    $domain = $explodedLine[1];
                    pihole_execute("-a removecustomdns ".$ip." ".$domain);
                }
            }
            catch (\Exception $ex)
            {
                return errorJsonResponse($ex->getMessage());
            fclose($handle);
        }
        $input = html_entity_decode(trim($_POST['name']));
        $names = str_getcsv($input, ' ');
        $name = html_entity_decode($_POST['name']);
        $desc = html_entity_decode($_POST['desc']);

        if (!$stmt->bindValue(':name', $name, SQLITE3_TEXT)) {
            $comment = html_entity_decode($_POST['comment']);
        $comment = html_entity_decode($_POST['comment']);
        $domains = explode(' ', html_entity_decode(trim($_POST['domain'])));
        $comment = html_entity_decode($_POST['comment']);
        $comment = html_entity_decode($_POST['comment']);
        $addresses = explode(' ', html_entity_decode(trim($_POST['address'])));
        $comment = html_entity_decode($_POST['comment']);
        $comment = html_entity_decode($_POST['comment']);
		if(isset($field) && strlen($row[$field]) > 253)
			$stmt->bindValue(":".$key, htmlentities($value), $sqltype);
                                                            <td id="HOST"><?php echo htmlentities($lease["host"]); ?></td>
                                                            <td><?php echo htmlentities($lease["host"]); ?></td>