add new IPTools class; add new callback to show link to domain in domain-overview; validate possible allowed_ip-ranges in FroxlorRPC; fix possible duplicate ips for mysql-access-host in installation

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2022-04-22 10:36:46 +02:00
parent d3ae4c5d72
commit b869c84f4d
11 changed files with 267 additions and 64 deletions

View File

@@ -45,6 +45,7 @@
"ext-json": "*",
"ext-openssl": "*",
"ext-fileinfo": "*",
"ext-gmp": "*",
"phpmailer/phpmailer": "~6.0",
"monolog/monolog": "^1.24",
"robthree/twofactorauth": "^1.6",

View File

@@ -779,6 +779,8 @@ class FroxlorInstall
$mysql_access_host_array[] = $this->_data['serverip'];
}
$mysql_access_host_array = array_unique($mysql_access_host_array);
foreach ($mysql_access_host_array as $mysql_access_host) {
$frox_db = str_replace('`', '', $this->_data['mysql_database']);
$this->_grantDbPrivilegesTo($db_root, $frox_db, $this->_data['mysql_unpriv_user'], $this->_data['mysql_unpriv_pass'], $mysql_access_host);

View File

@@ -1,7 +1,7 @@
<?php
use Froxlor\Database\Database;
use Froxlor\Settings;
use Froxlor\Validate\Validate;
use Froxlor\System\IPTools;
/**
* This file is part of the Froxlor project.
@@ -892,7 +892,7 @@ if (\Froxlor\Froxlor::isDatabaseVersion('202107210')) {
Database::pexecute($result_stmt);
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_IPSANDPORTS . "` SET `ip` = :ip WHERE `id` = :id");
while ($iprow = $result_stmt->fetch(\PDO::FETCH_ASSOC)) {
if (Validate::is_ipv6($iprow['ip'])) {
if (IPTools::is_ipv6($iprow['ip'])) {
$ip = inet_ntop(inet_pton($iprow['ip']));
Database::pexecute($upd_stmt, [
'ip' => $ip,

View File

@@ -3,6 +3,8 @@
namespace Froxlor\Api;
use Exception;
use Froxlor\Database\Database;
use Froxlor\System\IPTools;
/**
* This file is part of the Froxlor project.
@@ -60,11 +62,12 @@ class FroxlorRPC
*
* @param string $key
* @param string $secret
* @return boolean
*
* @return bool
*/
private static function validateAuth(string $key, string $secret): bool
{
$sel_stmt = \Froxlor\Database\Database::prepare(
$sel_stmt = Database::prepare(
"
SELECT ak.*, a.api_allowed as admin_api_allowed, c.api_allowed as cust_api_allowed, c.deactivated
FROM `api_keys` ak
@@ -73,7 +76,7 @@ class FroxlorRPC
WHERE `apikey` = :ak AND `secret` = :as
"
);
$result = \Froxlor\Database\Database::pexecute_first($sel_stmt, array(
$result = Database::pexecute_first($sel_stmt, array(
'ak' => $key,
'as' => $secret
), true, true);
@@ -83,8 +86,7 @@ class FroxlorRPC
if (!empty($result['allowed_from'])) {
// @todo allow specification and validating of whole subnets later
$ip_list = explode(",", $result['allowed_from']);
$access_ip = inet_ntop(inet_pton($_SERVER['REMOTE_ADDR']));
if (in_array($access_ip, $ip_list)) {
if (self::validateAllowedFrom($ip_list, $_SERVER['REMOTE_ADDR'])) {
return true;
}
} else {
@@ -95,6 +97,32 @@ class FroxlorRPC
throw new Exception('Invalid authorization credentials', 403);
}
/**
* validate if given remote_addr is within the list of allowed ip/ip-ranges
*
* @param array $allowed_from
* @param string $remote_addr
*
* @return bool
*/
private static function validateAllowedFrom(array $allowed_from, string $remote_addr): bool
{
// shorten IP for comparison
$remote_addr = inet_ntop(inet_pton($remote_addr));
// check for diret matches
if (in_array($remote_addr, $allowed_from)) {
return true;
}
// check for possible cidr ranges
foreach ($allowed_from as $ip) {
$ip_cidr = explode("/", $ip);
if (count($ip_cidr) == 2 && IPTools::ip_in_range($ip_cidr, $remote_addr)) {
return true;
}
}
return false;
}
/**
* validates the given command
*

View File

@@ -266,7 +266,7 @@ class Store
if (count($ip_cidr) === 2) {
$ip = $ip_cidr[0];
if (strlen($ip_cidr[1]) <= 2) {
$ip_cidr[1] = \Froxlor\Validate\Validate::cidr2NetmaskAddr($org_ip);
$ip_cidr[1] = \Froxlor\System\IPTools::cidr2NetmaskAddr($org_ip);
}
$newfieldvalue[] = $ip . '/' . $ip_cidr[1];
} else {

View File

@@ -0,0 +1,165 @@
<?php
namespace Froxlor\System;
class IPTools
{
/**
* Converts CIDR to a netmask address
*
* @thx to https://stackoverflow.com/a/5711080/3020926
* @param string $cidr
*
* @return string
*/
public static function cidr2NetmaskAddr($cidr)
{
$ta = substr($cidr, strpos($cidr, '/') + 1) * 1;
$netmask = str_split(str_pad(str_pad('', $ta, '1'), 32, '0'), 8);
foreach ($netmask as &$element) {
$element = bindec($element);
}
return implode('.', $netmask);
}
/**
* Checks if an $address (IP) is IPv6
*
* @param string $address
*
* @return string|bool ip address on success, false on failure
*/
public static function is_ipv6($address)
{
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
/**
* Checks whether the given $ip is in range of given ip/cidr range
*
* @param array $ip_cidr 0 => ip, 1 => netmask in decimal, e.g. [0 => '123.123.123.123', 1 => 24]
* @param string $ip ip-address to check
*
* @return bool
*/
public static function ip_in_range(array $ip_cidr, string $ip): bool
{
$netip = $ip_cidr[0];
if (self::is_ipv6($netip)) {
return self::ipv6_in_range($ip_cidr, $ip);
}
$netmask = $ip_cidr[1];
$range_decimal = ip2long($netip);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, (32 - $netmask)) - 1;
$netmask_decimal = ~$wildcard_decimal;
return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
}
/**
* Checks whether the given ipv6 $ip is in range of given ip/cidr range
*
* @param array $ip_cidr 0 => ip, 1 => netmask in decimal, e.g. [0 => '123:123::1', 1 => 64]
* @param string $ip ip-address to check
*
* @return bool
*/
private static function ipv6_in_range(array $ip_cidr, string $ip): bool
{
$in_range = false;
$size = 128 - $ip_cidr[1];
if ($size == 0) {
return inet_ntop(inet_pton($ip_cidr[0])) == inet_ntop(inet_pton($ip));
}
$addr = gmp_init('0x' . str_replace(':', '', self::inet6_expand($ip_cidr[0])));
$mask = gmp_init('0x' . str_replace(':', '', self::inet6_expand(self::inet6_prefix_to_mask($ip_cidr[1]))));
$prefix = gmp_and($addr, $mask);
$start = gmp_strval(gmp_add($prefix, '0x1'), 16);
$end = '0b';
for ($i = 0; $i < $size; $i++) {
$end .= '1';
}
$end = gmp_strval(gmp_add($prefix, gmp_init($end)), 16);
$start_result = '';
for ($i = 0; $i < 8; $i++) {
$start_result .= substr($start, $i * 4, 4);
if ($i != 7) $start_result .= ':';
}
$end_result = '';
for ($i = 0; $i < 8; $i++) {
$end_result .= substr($end, $i * 4, 4);
if ($i != 7) $end_result .= ':';
}
$first = self::ip2long6($start_result);
$last = self::ip2long6($end_result);
$ip = self::ip2long6($ip);
$in_range = ($ip >= $first && $ip <= $last);
return $in_range;
}
private static function ip2long6($ip)
{
$ip_n = inet_pton($ip);
$bits = 15; // 16 x 8 bit = 128bit
$ipv6long = '';
while ($bits >= 0) {
$bin = sprintf("%08b", (ord($ip_n[$bits])));
$ipv6long = $bin . $ipv6long;
$bits--;
}
return gmp_strval(gmp_init($ipv6long, 2), 10);
}
private static function inet6_expand(string $addr)
{
// Check if there are segments missing, insert if necessary
if (strpos($addr, '::') !== false) {
$part = explode('::', $addr);
$part[0] = explode(':', $part[0]);
$part[1] = explode(':', $part[1]);
$missing = array();
for ($i = 0; $i < (8 - (count($part[0]) + count($part[1]))); $i++)
array_push($missing, '0000');
$missing = array_merge($part[0], $missing);
$part = array_merge($missing, $part[1]);
} else {
$part = explode(":", $addr);
}
// Pad each segment until it has 4 digits
foreach ($part as &$p) {
while (strlen($p) < 4) $p = '0' . $p;
}
unset($p);
// Join segments
$result = implode(':', $part);
// Quick check to make sure the length is as expected
if (strlen($result) == 39) {
return $result;
} else {
return false;
}
}
private static function inet6_prefix_to_mask($prefix)
{
/* Make sure the prefix is a number between 1 and 127 (inclusive) */
$prefix = intval($prefix);
if ($prefix < 0 || $prefix > 128) return false;
$mask = '0b';
for ($i = 0; $i < $prefix; $i++) $mask .= '1';
for ($i = strlen($mask) - 2; $i < 128; $i++) $mask .= '0';
$mask = gmp_strval(gmp_init($mask), 16);
$result = '';
for ($i = 0; $i < 8; $i++) {
$result .= substr($mask, $i * 4, 4);
if ($i != 7) $result .= ':';
} // for
return inet_ntop(inet_pton($result));
}
}

View File

@@ -61,6 +61,11 @@ class Domain
return UI::getLng('domains.aliasdomain') . ' ' . $attributes['fields']['aliasdomain'];
}
public static function domainExternalLink(array $attributes)
{
return '<a href="http://' . $attributes['data'] . '" target="_blank">' . $attributes['data'] . '</a>';
}
public static function canEdit(array $attributes): bool
{
return (bool)$attributes['fields']['caneditdomain'];

View File

@@ -1,6 +1,9 @@
<?php
namespace Froxlor\Validate;
use Froxlor\System\IPTools;
class Validate
{
@@ -31,7 +34,7 @@ class Validate
*/
public static function validate($str, $fieldname, $pattern = '', $lng = '', $emptydefault = array(), $throw_exception = false)
{
if (! is_array($emptydefault)) {
if (!is_array($emptydefault)) {
$emptydefault_array = array(
$emptydefault
);
@@ -41,7 +44,7 @@ class Validate
}
// Check if the $str is one of the values which represent the default for an 'empty' value
if (is_array($emptydefault) && ! empty($emptydefault) && in_array($str, $emptydefault)) {
if (is_array($emptydefault) && !empty($emptydefault) && in_array($str, $emptydefault)) {
return $str;
}
@@ -49,7 +52,7 @@ class Validate
$pattern = '/^[^\r\n\t\f\0]*$/D';
if (! preg_match($pattern, $str)) {
if (!preg_match($pattern, $str)) {
// Allows letters a-z, digits, space (\\040), hyphen (\\-), underscore (\\_) and backslash (\\\\),
// everything else is removed from the string.
$allowed = "/[^a-z0-9\\040\\.\\-\\_\\\\]/i";
@@ -70,38 +73,6 @@ class Validate
\Froxlor\UI\Response::standard_error($lng, $fieldname, $throw_exception);
}
/**
* Converts CIDR to a netmask address
*
* @thx to https://stackoverflow.com/a/5711080/3020926
* @param string $cidr
*
* @return string
*/
public static function cidr2NetmaskAddr($cidr)
{
$ta = substr($cidr, strpos($cidr, '/') + 1) * 1;
$netmask = str_split(str_pad(str_pad('', $ta, '1'), 32, '0'), 8);
foreach ($netmask as &$element) {
$element = bindec($element);
}
return implode('.', $netmask);
}
/**
* Checks if an $address (IP) is IPv6
*
* @param string $address
*
* @return string|bool ip address on success, false on failure
*/
public static function is_ipv6($address)
{
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
/**
* Checks whether it is a valid ip
*
@@ -131,10 +102,14 @@ class Validate
$org_ip = $ip;
$ip_cidr = explode("/", $ip);
if (count($ip_cidr) === 2) {
if (strlen($ip_cidr[1]) <= 2 && in_array((int) $ip_cidr[1], array_values(range(1, 32)), true) === false) {
$cidr_range_max = 32;
if (IPTools::is_ipv6($ip_cidr[0])) {
$cidr_range_max = 128;
}
if (strlen($ip_cidr[1]) <= 3 && in_array((int) $ip_cidr[1], array_values(range(1, $cidr_range_max)), true) === false) {
\Froxlor\UI\Response::standard_error($lng, $ip, $throw_exception);
}
if ($cidr_as_netmask && self::is_ipv6($ip_cidr[0])) {
if ($cidr_as_netmask && IPTools::is_ipv6($ip_cidr[0])) {
// MySQL does not handle CIDR of IPv6 addresses, return error
if ($return_bool) {
return false;
@@ -143,8 +118,8 @@ class Validate
}
}
$ip = $ip_cidr[0];
if ($cidr_as_netmask && strlen($ip_cidr[1]) <= 2) {
$ip_cidr[1] = self::cidr2NetmaskAddr($org_ip);
if ($cidr_as_netmask && strlen($ip_cidr[1]) <= 3) {
$ip_cidr[1] = IPTools::cidr2NetmaskAddr($org_ip);
}
$cidr = "/" . $ip_cidr[1];
} else {
@@ -279,10 +254,10 @@ class Validate
*/
public static function validateUsername($username, $unix_names = 1, $mysql_max = '')
{
if (empty($mysql_max) || ! is_numeric($mysql_max) || $mysql_max <= 0) {
if (empty($mysql_max) || !is_numeric($mysql_max) || $mysql_max <= 0) {
$mysql_max = \Froxlor\Database\Database::getSqlUsernameLength() - 1;
} else {
$mysql_max --;
$mysql_max--;
}
if ($unix_names == 0) {
if (strpos($username, '--') === false) {
@@ -302,7 +277,7 @@ class Validate
*/
public static function validateSqlInterval($interval = null)
{
if (! empty($interval) && strstr($interval, ' ') !== false) {
if (!empty($interval) && strstr($interval, ' ') !== false) {
/*
* [0] = ([0-9]+)
* [1] = valid SQL-Interval expression

View File

@@ -31,6 +31,7 @@ return [
'd.domain_ace' => [
'label' => $lng['domains']['domainname'],
'field' => 'domain_ace',
'callback' => [Domain::class, 'domainExternalLink'],
],
'd.documentroot' => [
'label' => $lng['panel']['path'],

View File

@@ -0,0 +1,40 @@
<?php
use PHPUnit\Framework\TestCase;
use Froxlor\System\IPTools;
/**
*
* @covers \Froxlor\System\IPTools
*/
class IPToolsTest extends TestCase
{
public function testValidateIPv6()
{
$result = IPTools::is_ipv6('1.1.1.1/4');
$this->assertFalse($result);
$result = IPTools::is_ipv6('1.1.1.1');
$this->assertFalse($result);
$result = IPTools::is_ipv6('::ffff:10.20.30.40');
$this->assertEquals('::ffff:10.20.30.40', $result);
$result = IPTools::is_ipv6('2620:0:2d0:200::7/32');
$this->assertFalse($result);
$result = IPTools::is_ipv6('2620:0:2d0:200::7');
$this->assertEquals('2620:0:2d0:200::7', $result);
}
public function testValidateIPinRange()
{
$result = IPTools::ip_in_range([0=>'82.149.225.46',1=>24], '123.213.132.1');
$this->assertFalse($result);
$result = IPTools::ip_in_range([0=>'82.149.225.46',1=>24], '2620:0:2d0:200::7');
$this->assertFalse($result);
$result = IPTools::ip_in_range([0=>'82.149.225.46',1=>24], '82.149.225.152');
$this->assertTrue($result);
$result = IPTools::ip_in_range([0=>'2620:0:2d0:200::1',1=>116], '2620:0:2d0:200::fff1');
$this->assertFalse($result);
$result = IPTools::ip_in_range([0=>'2620:0:2d0:200::1',1=>64], '2620:0:2d0:200::fff1');
$this->assertTrue($result);
}
}

View File

@@ -109,20 +109,6 @@ class ValidateTest extends TestCase
$this->assertEquals("8.8.8.8/128.0.0.0", $result);
}
public function testValidateIPv6()
{
$result = Validate::is_ipv6('1.1.1.1/4');
$this->assertFalse($result);
$result = Validate::is_ipv6('1.1.1.1');
$this->assertFalse($result);
$result = Validate::is_ipv6('::ffff:10.20.30.40');
$this->assertEquals('::ffff:10.20.30.40', $result);
$result = Validate::is_ipv6('2620:0:2d0:200::7/32');
$this->assertFalse($result);
$result = Validate::is_ipv6('2620:0:2d0:200::7');
$this->assertEquals('2620:0:2d0:200::7', $result);
}
public function testValidateIpLocalhostAllowedWrongIp()
{
$this->expectException("Exception");