diff --git a/sysutils/pfSense-pkg-System_Patches/Makefile b/sysutils/pfSense-pkg-System_Patches/Makefile index 4b4bee649098..eb035f05328d 100644 --- a/sysutils/pfSense-pkg-System_Patches/Makefile +++ b/sysutils/pfSense-pkg-System_Patches/Makefile @@ -1,7 +1,7 @@ # $FreeBSD$ PORTNAME= pfSense-pkg-System_Patches -DISTVERSION= ${PRODUCT_VERSION} +DISTVERSION= 2.2.21 CATEGORIES= sysutils MASTER_SITES= # empty DISTFILES= # empty diff --git a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/0ff75cd0b9fb14c04c94c3585831a9f669be0a5d.patch b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/0ff75cd0b9fb14c04c94c3585831a9f669be0a5d.patch new file mode 100644 index 000000000000..77b52938dfa5 --- /dev/null +++ b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/0ff75cd0b9fb14c04c94c3585831a9f669be0a5d.patch @@ -0,0 +1,48 @@ +commit 0ff75cd0b9fb14c04c94c3585831a9f669be0a5d +Author: jim-p +Date: Tue Apr 1 14:26:41 2025 -0400 + + Validation and output encoding of IPsec P1 interface. Fixes #16115 + +diff --git a/src/usr/local/www/vpn_ipsec.php b/src/usr/local/www/vpn_ipsec.php +index 90b8fcc9cd..c23d7da19d 100644 +--- a/src/usr/local/www/vpn_ipsec.php ++++ b/src/usr/local/www/vpn_ipsec.php +@@ -351,7 +351,7 @@ $i = 0; foreach (config_get_path('ipsec/phase1', []) as $ph1ent): + "; + if (!isset($ph1ent['mobile'])) { +- echo $if."
".$ph1ent['remote-gateway']; ++ echo $ph1ent['remote-gateway']; + } else { +- echo $if."
" . gettext("Mobile Clients") . ""; ++ echo "" . gettext("Mobile Clients") . ""; + } + ?> + +diff --git a/src/usr/local/www/vpn_ipsec_phase1.php b/src/usr/local/www/vpn_ipsec_phase1.php +index af1549bbb7..4897369708 100644 +--- a/src/usr/local/www/vpn_ipsec_phase1.php ++++ b/src/usr/local/www/vpn_ipsec_phase1.php +@@ -216,6 +216,10 @@ if ($_POST['save']) { + + /* input validation */ + ++ if (!array_key_exists($pconfig['interface'], build_interface_list())) { ++ $input_errors[] = gettext("Invalid interface."); ++ } ++ + $method = $pconfig['authentication_method']; + + // Unset ca and cert if not required to avoid storing in config diff --git a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15844-widgetkey-validation-24.11.patch b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15844-widgetkey-validation-24.11.patch new file mode 100644 index 000000000000..9bfa0d073a75 --- /dev/null +++ b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15844-widgetkey-validation-24.11.patch @@ -0,0 +1,639 @@ +diff --git a/src/etc/inc/util.inc b/src/etc/inc/util.inc +index 6cd0c821e315cd885e6b4c6483528fdc4f474001..25eec9e8a218d449893a6b6112aaff4b4b9b8148 100644 +--- a/src/etc/inc/util.inc ++++ b/src/etc/inc/util.inc +@@ -5166,3 +5166,66 @@ function unserialize_data(?string $path, mixed $default = null, ?array $options + return $data; + } + ++/* Get an array of active widgets and metadata from user settings */ ++function get_active_widgets($user_settings) { ++ $widgets = []; ++ ++ /* Break up the sequence string into an array of widget definitions */ ++ $widget_sep = ','; ++ $widget_seq_array = explode($widget_sep, rtrim($user_settings['widgets']['sequence'], $widget_sep)); ++ ++ foreach ($widget_seq_array as $widget_seq_data) { ++ /* Break each widget definition into its component values */ ++ [$name, $column, $display, $instance] = explode(':', $widget_seq_data); ++ if (is_null($instance)) { ++ $instance = 0; ++ } ++ $widgets[] = [ ++ 'name' => $name, ++ 'column' => $column, ++ 'display' => $display, ++ 'instance' => $instance ++ ]; ++ } ++ return $widgets; ++} ++ ++/* Test the validity of a given widget key based on user settings. */ ++function is_valid_widgetkey($widgetkey, $user_settings, $widgetfile = null) { ++ /* Proper form of a widgetkey is - ++ * Where: ++ * widget-name : Name of an active widget, which should be found in ++ * the current sequence list. ++ * instance-id : An integer 0 or higher identifying a widget instance ++ * ++ * Additionally, for a widget to be valid in this context it must also ++ * be present on the current Dashboard layout. ++ */ ++ ++ /* Break the given widgetkey into its component parts */ ++ [$wname, $wid] = explode('-', $widgetkey, 2); ++ ++ /* Test for basic validity conditions */ ++ if (empty($wname) || ++ !is_numericint($wid) || ++ empty($user_settings)) { ++ return false; ++ } ++ ++ /* Check if this widget also matches a specific widget name */ ++ if (!empty($widgetfile) && ++ ($wname != basename($widgetfile, '.widget.php'))) { ++ return false; ++ } ++ ++ /* Ensure the key is for a widget which is in the Dashboard ++ * configuration. */ ++ $widgets = get_active_widgets($user_settings); ++ foreach ($widgets as $widget) { ++ if (($widget['name'] == $wname) && ++ ($widget['instance'] == $wid)) { ++ return true; ++ } ++ } ++ return false; ++} +diff --git a/src/usr/local/www/guiconfig.inc b/src/usr/local/www/guiconfig.inc +index 051754b8c47117ccf251bb1002915721f40a19a8..22e876c3c36ce084229e6630c651583ab0fea7fc 100644 +--- a/src/usr/local/www/guiconfig.inc ++++ b/src/usr/local/www/guiconfig.inc +@@ -599,6 +599,9 @@ function gen_customwidgettitle_div($widgettitle) { + } + + function set_customwidgettitle(& $user_settings) { ++ if (!is_valid_widgetkey($_POST['widgetkey'], $user_settings)) { ++ return false; ++ } + if ($_POST['descr']) { + $user_settings['widgets'][$_POST['widgetkey']]['descr'] = trim($_POST['descr']); + } else { +diff --git a/src/usr/local/www/widgets/widgets/disks.widget.php b/src/usr/local/www/widgets/widgets/disks.widget.php +index 4e1b1116151639b19242082fcbeba433e11d7640..37d8e228a5499b5581f0a2a7540bc8d5117d2c4f 100644 +--- a/src/usr/local/www/widgets/widgets/disks.widget.php ++++ b/src/usr/local/www/widgets/widgets/disks.widget.php +@@ -25,13 +25,27 @@ require_once('vendor/autoload.php'); + // pfSense includes + require_once('guiconfig.inc'); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Widget includes + require_once('/usr/local/www/widgets/include/disks.inc'); + + global $disks_widget_defaults; + +-$widgetkey = (isset($_POST['widgetkey'])) ? $_POST['widgetkey'] : $widgetkey; +- + // Now override any defaults with user settings + $widget_config = array_replace($disks_widget_defaults, (array) $user_settings['widgets'][$widgetkey]); + +diff --git a/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php b/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php +index fc00426c88bb94eca6640b0ad5839fcc2155458e..a55ce2452e687fd771df7b1bea9bb416b44b55a1 100644 +--- a/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php ++++ b/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php +@@ -29,6 +29,22 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/dyn_dns_status.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Constructs a unique key that will identify a Dynamic DNS entry in the filter list. + if (!function_exists('get_dyndnsent_key')) { + function get_dyndnsent_key($dyndns) { +diff --git a/src/usr/local/www/widgets/widgets/gateways.widget.php b/src/usr/local/www/widgets/widgets/gateways.widget.php +index f03956b5974f8485c165d0174ee91787703deb57..70ca6e8875b6180e11c7d72941a97b7408d1b757 100644 +--- a/src/usr/local/www/widgets/widgets/gateways.widget.php ++++ b/src/usr/local/www/widgets/widgets/gateways.widget.php +@@ -31,9 +31,32 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/gateways.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ ++global $display_types; ++$display_types = array( ++ 'gw_ip' => gettext('Gateway IP Address'), ++ 'monitor_ip' => gettext('Monitor IP Address'), ++ 'both_ip' => gettext('Both') ++); ++ + if (!function_exists('compose_table_body_contents')) { + function compose_table_body_contents($widgetkey) { +- global $user_settings; ++ global $user_settings, $display_types; + + $rtnstr = ''; + +@@ -41,7 +63,8 @@ if (!function_exists('compose_table_body_contents')) { + $gateways_status = array(); + $gateways_status = return_gateways_status(true); + +- if (isset($user_settings["widgets"][$widgetkey]["display_type"])) { ++ if (isset($user_settings["widgets"][$widgetkey]["display_type"]) && ++ array_key_exists($user_settings["widgets"][$widgetkey]["display_type"], $display_types)) { + $display_type = $user_settings["widgets"][$widgetkey]["display_type"]; + } else { + $display_type = "gw_ip"; +@@ -211,7 +234,8 @@ if ($_POST['widgetkey']) { + $user_settings["widgets"][$_POST['widgetkey']] = array(); + } + +- if (isset($_POST["display_type"])) { ++ if (isset($_POST["display_type"]) && ++ array_key_exists($_POST["display_type"], $display_types)) { + $user_settings["widgets"][$_POST['widgetkey']]["display_type"] = $_POST["display_type"]; + } + +diff --git a/src/usr/local/www/widgets/widgets/interface_statistics.widget.php b/src/usr/local/www/widgets/widgets/interface_statistics.widget.php +index 45b4bbf977b72e5204c7b41e07dec8c7644addf1..2b12a83b57885f1328020d4bc8f557da4c5a0a22 100644 +--- a/src/usr/local/www/widgets/widgets/interface_statistics.widget.php ++++ b/src/usr/local/www/widgets/widgets/interface_statistics.widget.php +@@ -33,6 +33,27 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/interface_statistics.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ ++$orientations = array( ++ 'if_columns' => gettext('Each interface in a column'), ++ 'if_rows' => gettext('Each interface in a row') ++); ++ + $ifdescrs = get_configured_interface_with_descr(); + $ifstats = array( + 'inpkts' => gettext('Packets In'), +@@ -53,7 +73,8 @@ if ($_REQUEST && $_REQUEST['ajax']) { + $an_interface_is_displayed = false; // decide if at least 1 interface is displayed (i.e. not down) + $an_ifstat_is_displayed = false; + +- if (isset($user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"])) { ++ if (isset($user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"]) && ++ array_key_exists($user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"], $orientations)) { + $orientation_type = $user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"]; + } else { + $orientation_type = "if_columns"; +@@ -160,7 +181,8 @@ if ($_REQUEST && $_REQUEST['ajax']) { + } else if ($_POST['widgetkey']) { + set_customwidgettitle($user_settings); + +- if (isset($_POST['orientation_type'])) { ++ if (isset($_POST['orientation_type']) && ++ array_key_exists($_POST['orientation_type'], $orientations)) { + $user_settings['widgets'][$_POST['widgetkey']]['orientation_type'] = $_POST['orientation_type']; + } + +diff --git a/src/usr/local/www/widgets/widgets/interfaces.widget.php b/src/usr/local/www/widgets/widgets/interfaces.widget.php +index ad9d713070222c2b6fcaf13a26d05692f24d5b0e..42b7ffa2cefde53bfb733ab39d840492dc8d8bc0 100644 +--- a/src/usr/local/www/widgets/widgets/interfaces.widget.php ++++ b/src/usr/local/www/widgets/widgets/interfaces.widget.php +@@ -27,6 +27,22 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/interfaces.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $platform = system_identify_specific_platform(); + $ifdescrs = get_configured_interface_with_descr(); + $has_switch = array("1100", "2100", "3100", "7100", "doorkeeper"); +@@ -50,12 +65,6 @@ if ($_POST['widgetkey'] && !$_REQUEST['ajax']) { + header("Location: /index.php"); + } + +-// When this widget is included in the dashboard, $widgetkey is already defined before the widget is included. +-// When the ajax call is made to refresh the interfaces table, 'widgetkey' comes in $_REQUEST. +-if ($_REQUEST['widgetkey']) { +- $widgetkey = $_REQUEST['widgetkey']; +-} +- + ?> + +
+diff --git a/src/usr/local/www/widgets/widgets/ipsec.widget.php b/src/usr/local/www/widgets/widgets/ipsec.widget.php +index 27273963d7ce45dcbd678cd51debf0b3ab8e929b..f7d92f8bc12dd9e29d87ec68000dc2c7f676aae0 100644 +--- a/src/usr/local/www/widgets/widgets/ipsec.widget.php ++++ b/src/usr/local/www/widgets/widgets/ipsec.widget.php +@@ -33,6 +33,22 @@ require_once("functions.inc"); + require_once("service-utils.inc"); + require_once("ipsec.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Should always be initialized + $ipsec_widget_tabs = array( + 'overview' => gettext('Overview'), +diff --git a/src/usr/local/www/widgets/widgets/log.widget.php b/src/usr/local/www/widgets/widgets/log.widget.php +index c75cc1ae1ba484594ff94238cb7157378aca1903..d3445a35a5fad5c84e06704935a23ba397b5f527 100644 +--- a/src/usr/local/www/widgets/widgets/log.widget.php ++++ b/src/usr/local/www/widgets/widgets/log.widget.php +@@ -48,6 +48,22 @@ require_once("functions.inc"); + /* In an effort to reduce duplicate code, many shared functions have been moved here. */ + require_once("syslog.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + /* Enable or disable debugging (detail level depending on removed ^//DEBUG^statements */ + $DebugOn = false; + /* Debugging options */ +@@ -84,7 +99,9 @@ if ($_REQUEST['widgetkey'] && !$_REQUEST['ajax']) { + } + unset($acts); + +- if (($_POST['filterlogentriesinterfaces']) and ($_POST['filterlogentriesinterfaces'] != "All")) { ++ if ($_POST['filterlogentriesinterfaces'] && ++ ($_POST['filterlogentriesinterfaces'] != "All") && ++ array_key_exists($_POST['filterlogentriesinterfaces'], get_configured_interface_with_descr())) { + $user_settings['widgets'][$_POST['widgetkey']]['filterlogentriesinterfaces'] = trim($_POST['filterlogentriesinterfaces']); + } else { + unset($user_settings['widgets'][$_POST['widgetkey']]['filterlogentriesinterfaces']); +@@ -106,11 +123,6 @@ $date0 = new DateTime($date); + + if ($DebugOn) { $logContent .= date($dateFormat)."_^START^".PHP_EOL; } + +-// When this widget is included in the dashboard, $widgetkey is already defined before the widget is included. +-// When the ajax call is made to refresh the firewall log table, 'widgetkey' comes in $_REQUEST. +-if ($_REQUEST['widgetkey']) { +- $widgetkey = $_REQUEST['widgetkey']; +-} + //DEBUG: $logContent .= date($dateFormat)."_After request widgetkey".PHP_EOL; + + $iface_descr_arr = get_configured_interface_with_descr(); +@@ -130,7 +142,7 @@ $filterfieldsarray = array( + ); + //DEBUG: $logContent .= date($dateFormat)."_After filling_filter array".PHP_EOL; + +-$nentriesinterval = isset($user_settings['widgets'][$widgetkey]['filterlogentriesinterval']) ? $user_settings['widgets'][$widgetkey]['filterlogentriesinterval'] : 60; ++$nentriesinterval = is_numeric($user_settings['widgets'][$widgetkey]['filterlogentriesinterval']) ? $user_settings['widgets'][$widgetkey]['filterlogentriesinterval'] : 60; + //DEBUG: $logContent .= date($dateFormat)."_After entries_interval".PHP_EOL; + + $filter_logfile = "{$g['varlog_path']}/filter.log"; +diff --git a/src/usr/local/www/widgets/widgets/openvpn.widget.php b/src/usr/local/www/widgets/widgets/openvpn.widget.php +index 12f2d399dd246e533006745e56e2b587e69878a4..514d73ca71f2cb588c115a05f3b89768047d75e0 100644 +--- a/src/usr/local/www/widgets/widgets/openvpn.widget.php ++++ b/src/usr/local/www/widgets/widgets/openvpn.widget.php +@@ -24,6 +24,22 @@ + require_once("guiconfig.inc"); + require_once("openvpn.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Output the widget panel from this function so that it can be called from the AJAX handler as well as + // when first rendering the page + if (!function_exists('printPanel')) { +diff --git a/src/usr/local/www/widgets/widgets/picture.widget.php b/src/usr/local/www/widgets/widgets/picture.widget.php +index 82ac26024cc215caf2bc1507eb61a05db6c7b83e..7f7d609e6e4c97a44f4aa80311dc9d4f04f8e4aa 100644 +--- a/src/usr/local/www/widgets/widgets/picture.widget.php ++++ b/src/usr/local/www/widgets/widgets/picture.widget.php +@@ -25,6 +25,21 @@ require_once("guiconfig.inc"); + require_once("pfsense-utils.inc"); + require_once("functions.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} + + if ($_GET['getpic']=="true") { + $wk = basename($_GET['widgetkey']); +diff --git a/src/usr/local/www/widgets/widgets/rss.widget.php b/src/usr/local/www/widgets/widgets/rss.widget.php +index 92bf925b76ee87eb8b17ca6a69dc7705f5215f73..cf4f79dcb102d9996fcc89fe947ae08ac3d062b3 100644 +--- a/src/usr/local/www/widgets/widgets/rss.widget.php ++++ b/src/usr/local/www/widgets/widgets/rss.widget.php +@@ -25,6 +25,22 @@ require_once("guiconfig.inc"); + require_once("pfsense-utils.inc"); + require_once("functions.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + /* bring in the Composer autoloader */ + require_once('vendor/autoload.php'); + +@@ -32,10 +47,37 @@ use SimplePie\SimplePie; + + if ($_POST['widgetkey']) { + set_customwidgettitle($user_settings); +- $user_settings['widgets'][$_POST['widgetkey']]['rssfeed'] = str_replace("\n", ",", htmlspecialchars($_POST['rssfeed'], ENT_QUOTES | ENT_HTML401)); +- $user_settings['widgets'][$_POST['widgetkey']]['rssmaxitems'] = str_replace("\n", ",", htmlspecialchars($_POST['rssmaxitems'], ENT_QUOTES | ENT_HTML401)); +- $user_settings['widgets'][$_POST['widgetkey']]['rsswidgetheight'] = htmlspecialchars($_POST['rsswidgetheight'], ENT_QUOTES | ENT_HTML401); +- $user_settings['widgets'][$_POST['widgetkey']]['rsswidgettextlength'] = htmlspecialchars($_POST['rsswidgettextlength'], ENT_QUOTES | ENT_HTML401); ++ ++ if ($_POST['rssfeed']) { ++ $validfeeds = []; ++ /* Allow feeds separated by comma or newline */ ++ $feeds = preg_split('/[,\n]/', $_POST['rssfeed']); ++ foreach ($feeds as $feed) { ++ /* Trim any extra whitespace as the submitted value may have \r at the end. */ ++ $feed = trim($feed); ++ if (is_URL($feed)) { ++ $validfeeds[] = $feed; ++ } ++ } ++ $user_settings['widgets'][$_POST['widgetkey']]['rssfeed'] = htmlspecialchars(implode(",", $validfeeds), ENT_QUOTES | ENT_HTML401); ++ } ++ ++ if (is_numeric($_POST['rssmaxitems'])) { ++ $user_settings['widgets'][$_POST['widgetkey']]['rssmaxitems'] = $_POST['rssmaxitems']; ++ } else { ++ unset($user_settings['widgets'][$_POST['widgetkey']]['rssmaxitems']); ++ } ++ if (is_numeric($_POST['rsswidgetheight'])) { ++ $user_settings['widgets'][$_POST['widgetkey']]['rsswidgetheight'] = $_POST['rsswidgetheight']; ++ } else { ++ unset($user_settings['widgets'][$_POST['widgetkey']]['rsswidgetheight']); ++ } ++ if (is_numeric($_POST['rsswidgettextlength'])) { ++ $user_settings['widgets'][$_POST['widgetkey']]['rsswidgettextlength'] = $_POST['rsswidgettextlength']; ++ } else { ++ unset($user_settings['widgets'][$_POST['widgetkey']]['rsswidgettextlength']); ++ } ++ + save_widget_settings($_SESSION['Username'], $user_settings["widgets"], gettext("Saved RSS Widget feed via Dashboard.")); + header("Location: /"); + } +diff --git a/src/usr/local/www/widgets/widgets/services_status.widget.php b/src/usr/local/www/widgets/widgets/services_status.widget.php +index 0e316d77f9241f77865d6284d75dabd0034ddba7..04dbaba8686ef39dea6ad90fccf06959a139c97e 100644 +--- a/src/usr/local/www/widgets/widgets/services_status.widget.php ++++ b/src/usr/local/www/widgets/widgets/services_status.widget.php +@@ -29,6 +29,22 @@ require_once("ipsec.inc"); + require_once("vpn.inc"); + require_once("/usr/local/www/widgets/include/services_status.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $services = get_services(); + + $numsvcs = count($services); +diff --git a/src/usr/local/www/widgets/widgets/smart_status.widget.php b/src/usr/local/www/widgets/widgets/smart_status.widget.php +index e9e2502df474b2d9e7bb8b94af47f2893e580d9f..d97dde69ec14168be34bb0768bb2887d64929957 100644 +--- a/src/usr/local/www/widgets/widgets/smart_status.widget.php ++++ b/src/usr/local/www/widgets/widgets/smart_status.widget.php +@@ -30,6 +30,23 @@ require_once("guiconfig.inc"); + require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/smart_status.inc"); ++ ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $specplatform = system_identify_specific_platform(); + + $devs = array(); +diff --git a/src/usr/local/www/widgets/widgets/system_information.widget.php b/src/usr/local/www/widgets/widgets/system_information.widget.php +index cf69ee3b7fb57c32e7bce323740832831ec7628a..bee990a8d586888d5c810463a61813781b17bed6 100644 +--- a/src/usr/local/www/widgets/widgets/system_information.widget.php ++++ b/src/usr/local/www/widgets/widgets/system_information.widget.php +@@ -32,6 +32,22 @@ require_once('notices.inc'); + require_once('system.inc'); + include_once("includes/functions.inc.php"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $sysinfo_items = array( + 'name' => gettext('Name'), + 'user' => gettext('User'), +diff --git a/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php b/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php +index 0f21af304f83ee1d4f48b7a5150af3d39013b66d..17374449cd2ec8e8203d19fd39aaff4f9d07c87a 100644 +--- a/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php ++++ b/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php +@@ -24,6 +24,21 @@ + require_once("guiconfig.inc"); + require_once("system.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} + + //========================================================================= + //called by showThermalSensorsData() (jQuery Ajax call) in thermal_sensors.js +diff --git a/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php b/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php +index ef3f5d52283eef933b12ea5e15e22b9ab4302cf6..9f045a153b89512df92edc642b6c789d8dcf8402 100644 +--- a/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php ++++ b/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php +@@ -26,6 +26,22 @@ require_once("guiconfig.inc"); + require_once("system.inc"); + require_once("/usr/local/www/widgets/include/wake_on_lan.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $wolcomputers = config_get_path('wol/wolentry', []); + + // Constructs a unique key that will identify a WoL entry in the filter list. diff --git a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15844-widgetkey-validation-ce-2.7.2.patch b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15844-widgetkey-validation-ce-2.7.2.patch new file mode 100644 index 000000000000..ba893e82d2a9 --- /dev/null +++ b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15844-widgetkey-validation-ce-2.7.2.patch @@ -0,0 +1,639 @@ +diff --git a/src/etc/inc/util.inc b/src/etc/inc/util.inc +index 6cd0c821e315cd885e6b4c6483528fdc4f474001..25eec9e8a218d449893a6b6112aaff4b4b9b8148 100644 +--- a/src/etc/inc/util.inc ++++ b/src/etc/inc/util.inc +@@ -5166,3 +5166,66 @@ function unserialize_data(?string $path, mixed $default = null, ?array $options + return $data; + } + ++/* Get an array of active widgets and metadata from user settings */ ++function get_active_widgets($user_settings) { ++ $widgets = []; ++ ++ /* Break up the sequence string into an array of widget definitions */ ++ $widget_sep = ','; ++ $widget_seq_array = explode($widget_sep, rtrim($user_settings['widgets']['sequence'], $widget_sep)); ++ ++ foreach ($widget_seq_array as $widget_seq_data) { ++ /* Break each widget definition into its component values */ ++ [$name, $column, $display, $instance] = explode(':', $widget_seq_data); ++ if (is_null($instance)) { ++ $instance = 0; ++ } ++ $widgets[] = [ ++ 'name' => $name, ++ 'column' => $column, ++ 'display' => $display, ++ 'instance' => $instance ++ ]; ++ } ++ return $widgets; ++} ++ ++/* Test the validity of a given widget key based on user settings. */ ++function is_valid_widgetkey($widgetkey, $user_settings, $widgetfile = null) { ++ /* Proper form of a widgetkey is - ++ * Where: ++ * widget-name : Name of an active widget, which should be found in ++ * the current sequence list. ++ * instance-id : An integer 0 or higher identifying a widget instance ++ * ++ * Additionally, for a widget to be valid in this context it must also ++ * be present on the current Dashboard layout. ++ */ ++ ++ /* Break the given widgetkey into its component parts */ ++ [$wname, $wid] = explode('-', $widgetkey, 2); ++ ++ /* Test for basic validity conditions */ ++ if (empty($wname) || ++ !is_numericint($wid) || ++ empty($user_settings)) { ++ return false; ++ } ++ ++ /* Check if this widget also matches a specific widget name */ ++ if (!empty($widgetfile) && ++ ($wname != basename($widgetfile, '.widget.php'))) { ++ return false; ++ } ++ ++ /* Ensure the key is for a widget which is in the Dashboard ++ * configuration. */ ++ $widgets = get_active_widgets($user_settings); ++ foreach ($widgets as $widget) { ++ if (($widget['name'] == $wname) && ++ ($widget['instance'] == $wid)) { ++ return true; ++ } ++ } ++ return false; ++} +diff --git a/src/usr/local/www/guiconfig.inc b/src/usr/local/www/guiconfig.inc +index 051754b8c47117ccf251bb1002915721f40a19a8..22e876c3c36ce084229e6630c651583ab0fea7fc 100644 +--- a/src/usr/local/www/guiconfig.inc ++++ b/src/usr/local/www/guiconfig.inc +@@ -599,6 +599,9 @@ function gen_customwidgettitle_div($widgettitle) { + } + + function set_customwidgettitle(& $user_settings) { ++ if (!is_valid_widgetkey($_POST['widgetkey'], $user_settings)) { ++ return false; ++ } + if ($_POST['descr']) { + $user_settings['widgets'][$_POST['widgetkey']]['descr'] = trim($_POST['descr']); + } else { +diff --git a/src/usr/local/www/widgets/widgets/disks.widget.php b/src/usr/local/www/widgets/widgets/disks.widget.php +index 4e1b1116151639b19242082fcbeba433e11d7640..37d8e228a5499b5581f0a2a7540bc8d5117d2c4f 100644 +--- a/src/usr/local/www/widgets/widgets/disks.widget.php ++++ b/src/usr/local/www/widgets/widgets/disks.widget.php +@@ -25,13 +25,27 @@ require_once('vendor/autoload.php'); + // pfSense includes + require_once('guiconfig.inc'); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Widget includes + require_once('/usr/local/www/widgets/include/disks.inc'); + + global $disks_widget_defaults; + +-$widgetkey = (isset($_POST['widgetkey'])) ? $_POST['widgetkey'] : $widgetkey; +- + // Now override any defaults with user settings + $widget_config = array_replace($disks_widget_defaults, (array) $user_settings['widgets'][$widgetkey]); + +diff --git a/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php b/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php +index fc00426c88bb94eca6640b0ad5839fcc2155458e..a55ce2452e687fd771df7b1bea9bb416b44b55a1 100644 +--- a/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php ++++ b/src/usr/local/www/widgets/widgets/dyn_dns_status.widget.php +@@ -29,6 +29,22 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/dyn_dns_status.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Constructs a unique key that will identify a Dynamic DNS entry in the filter list. + if (!function_exists('get_dyndnsent_key')) { + function get_dyndnsent_key($dyndns) { +diff --git a/src/usr/local/www/widgets/widgets/gateways.widget.php b/src/usr/local/www/widgets/widgets/gateways.widget.php +index f03956b5974f8485c165d0174ee91787703deb57..70ca6e8875b6180e11c7d72941a97b7408d1b757 100644 +--- a/src/usr/local/www/widgets/widgets/gateways.widget.php ++++ b/src/usr/local/www/widgets/widgets/gateways.widget.php +@@ -31,9 +31,32 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/gateways.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ ++global $display_types; ++$display_types = array( ++ 'gw_ip' => gettext('Gateway IP Address'), ++ 'monitor_ip' => gettext('Monitor IP Address'), ++ 'both_ip' => gettext('Both') ++); ++ + if (!function_exists('compose_table_body_contents')) { + function compose_table_body_contents($widgetkey) { +- global $user_settings; ++ global $user_settings, $display_types; + + $rtnstr = ''; + +@@ -41,7 +63,8 @@ if (!function_exists('compose_table_body_contents')) { + $gateways_status = array(); + $gateways_status = return_gateways_status(true); + +- if (isset($user_settings["widgets"][$widgetkey]["display_type"])) { ++ if (isset($user_settings["widgets"][$widgetkey]["display_type"]) && ++ array_key_exists($user_settings["widgets"][$widgetkey]["display_type"], $display_types)) { + $display_type = $user_settings["widgets"][$widgetkey]["display_type"]; + } else { + $display_type = "gw_ip"; +@@ -211,7 +234,8 @@ if ($_POST['widgetkey']) { + $user_settings["widgets"][$_POST['widgetkey']] = array(); + } + +- if (isset($_POST["display_type"])) { ++ if (isset($_POST["display_type"]) && ++ array_key_exists($_POST["display_type"], $display_types)) { + $user_settings["widgets"][$_POST['widgetkey']]["display_type"] = $_POST["display_type"]; + } + +diff --git a/src/usr/local/www/widgets/widgets/interface_statistics.widget.php b/src/usr/local/www/widgets/widgets/interface_statistics.widget.php +index 45b4bbf977b72e5204c7b41e07dec8c7644addf1..2b12a83b57885f1328020d4bc8f557da4c5a0a22 100644 +--- a/src/usr/local/www/widgets/widgets/interface_statistics.widget.php ++++ b/src/usr/local/www/widgets/widgets/interface_statistics.widget.php +@@ -33,6 +33,27 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/interface_statistics.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ ++$orientations = array( ++ 'if_columns' => gettext('Each interface in a column'), ++ 'if_rows' => gettext('Each interface in a row') ++); ++ + $ifdescrs = get_configured_interface_with_descr(); + $ifstats = array( + 'inpkts' => gettext('Packets In'), +@@ -53,7 +73,8 @@ if ($_REQUEST && $_REQUEST['ajax']) { + $an_interface_is_displayed = false; // decide if at least 1 interface is displayed (i.e. not down) + $an_ifstat_is_displayed = false; + +- if (isset($user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"])) { ++ if (isset($user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"]) && ++ array_key_exists($user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"], $orientations)) { + $orientation_type = $user_settings["widgets"][$_REQUEST['widgetkey']]["orientation_type"]; + } else { + $orientation_type = "if_columns"; +@@ -160,7 +181,8 @@ if ($_REQUEST && $_REQUEST['ajax']) { + } else if ($_POST['widgetkey']) { + set_customwidgettitle($user_settings); + +- if (isset($_POST['orientation_type'])) { ++ if (isset($_POST['orientation_type']) && ++ array_key_exists($_POST['orientation_type'], $orientations)) { + $user_settings['widgets'][$_POST['widgetkey']]['orientation_type'] = $_POST['orientation_type']; + } + +diff --git a/src/usr/local/www/widgets/widgets/interfaces.widget.php b/src/usr/local/www/widgets/widgets/interfaces.widget.php +index ad9d713070222c2b6fcaf13a26d05692f24d5b0e..42b7ffa2cefde53bfb733ab39d840492dc8d8bc0 100644 +--- a/src/usr/local/www/widgets/widgets/interfaces.widget.php ++++ b/src/usr/local/www/widgets/widgets/interfaces.widget.php +@@ -27,6 +27,22 @@ require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/interfaces.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $ifdescrs = get_configured_interface_with_descr(); + // Update once per minute by default, instead of every 10 seconds + $widgetperiod = config_get_path('widgets/period', 10) * 1000 * 6; +@@ -50,12 +65,6 @@ if ($_POST['widgetkey'] && !$_REQUEST['ajax']) { + header("Location: /index.php"); + } + +-// When this widget is included in the dashboard, $widgetkey is already defined before the widget is included. +-// When the ajax call is made to refresh the interfaces table, 'widgetkey' comes in $_REQUEST. +-if ($_REQUEST['widgetkey']) { +- $widgetkey = $_REQUEST['widgetkey']; +-} +- + ?> + +
+diff --git a/src/usr/local/www/widgets/widgets/ipsec.widget.php b/src/usr/local/www/widgets/widgets/ipsec.widget.php +index 27273963d7ce45dcbd678cd51debf0b3ab8e929b..f7d92f8bc12dd9e29d87ec68000dc2c7f676aae0 100644 +--- a/src/usr/local/www/widgets/widgets/ipsec.widget.php ++++ b/src/usr/local/www/widgets/widgets/ipsec.widget.php +@@ -33,6 +33,22 @@ require_once("functions.inc"); + require_once("service-utils.inc"); + require_once("ipsec.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Should always be initialized + $ipsec_widget_tabs = array( + 'overview' => gettext('Overview'), +diff --git a/src/usr/local/www/widgets/widgets/log.widget.php b/src/usr/local/www/widgets/widgets/log.widget.php +index c75cc1ae1ba484594ff94238cb7157378aca1903..d3445a35a5fad5c84e06704935a23ba397b5f527 100644 +--- a/src/usr/local/www/widgets/widgets/log.widget.php ++++ b/src/usr/local/www/widgets/widgets/log.widget.php +@@ -48,6 +48,22 @@ require_once("functions.inc"); + /* In an effort to reduce duplicate code, many shared functions have been moved here. */ + require_once("syslog.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + if ($_REQUEST['widgetkey'] && !$_REQUEST['ajax']) { + set_customwidgettitle($user_settings); + +@@ -84,7 +99,9 @@ if ($_REQUEST['widgetkey'] && !$_REQUEST['ajax']) { + } + unset($acts); + +- if (($_POST['filterlogentriesinterfaces']) and ($_POST['filterlogentriesinterfaces'] != "All")) { ++ if ($_POST['filterlogentriesinterfaces'] && ++ ($_POST['filterlogentriesinterfaces'] != "All") && ++ array_key_exists($_POST['filterlogentriesinterfaces'], get_configured_interface_with_descr())) { + $user_settings['widgets'][$_POST['widgetkey']]['filterlogentriesinterfaces'] = trim($_POST['filterlogentriesinterfaces']); + } else { + unset($user_settings['widgets'][$_POST['widgetkey']]['filterlogentriesinterfaces']); +@@ -106,11 +123,6 @@ $date0 = new DateTime($date); + + if ($DebugOn) { $logContent .= date($dateFormat)."_^START^".PHP_EOL; } + +-// When this widget is included in the dashboard, $widgetkey is already defined before the widget is included. +-// When the ajax call is made to refresh the firewall log table, 'widgetkey' comes in $_REQUEST. +-if ($_REQUEST['widgetkey']) { +- $widgetkey = $_REQUEST['widgetkey']; +-} + + $iface_descr_arr = get_configured_interface_with_descr(); + +@@ -130,7 +142,7 @@ $filterfieldsarray = array( + "interface" => isset($iface_descr_arr[$nentriesinterfaces]) ? $iface_descr_arr[$nentriesinterfaces] : $nentriesinterfaces + ); + +-$nentriesinterval = isset($user_settings['widgets'][$widgetkey]['filterlogentriesinterval']) ? $user_settings['widgets'][$widgetkey]['filterlogentriesinterval'] : 60; ++$nentriesinterval = is_numeric($user_settings['widgets'][$widgetkey]['filterlogentriesinterval']) ? $user_settings['widgets'][$widgetkey]['filterlogentriesinterval'] : 60; + + $filter_logfile = "{$g['varlog_path']}/filter.log"; + +diff --git a/src/usr/local/www/widgets/widgets/openvpn.widget.php b/src/usr/local/www/widgets/widgets/openvpn.widget.php +index 12f2d399dd246e533006745e56e2b587e69878a4..514d73ca71f2cb588c115a05f3b89768047d75e0 100644 +--- a/src/usr/local/www/widgets/widgets/openvpn.widget.php ++++ b/src/usr/local/www/widgets/widgets/openvpn.widget.php +@@ -24,6 +24,22 @@ + require_once("guiconfig.inc"); + require_once("openvpn.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + // Output the widget panel from this function so that it can be called from the AJAX handler as well as + // when first rendering the page + if (!function_exists('printPanel')) { +diff --git a/src/usr/local/www/widgets/widgets/picture.widget.php b/src/usr/local/www/widgets/widgets/picture.widget.php +index 82ac26024cc215caf2bc1507eb61a05db6c7b83e..7f7d609e6e4c97a44f4aa80311dc9d4f04f8e4aa 100644 +--- a/src/usr/local/www/widgets/widgets/picture.widget.php ++++ b/src/usr/local/www/widgets/widgets/picture.widget.php +@@ -25,6 +25,21 @@ require_once("guiconfig.inc"); + require_once("pfsense-utils.inc"); + require_once("functions.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} + + if ($_GET['getpic']=="true") { + $wk = basename($_GET['widgetkey']); +diff --git a/src/usr/local/www/widgets/widgets/rss.widget.php b/src/usr/local/www/widgets/widgets/rss.widget.php +index 92bf925b76ee87eb8b17ca6a69dc7705f5215f73..cf4f79dcb102d9996fcc89fe947ae08ac3d062b3 100644 +--- a/src/usr/local/www/widgets/widgets/rss.widget.php ++++ b/src/usr/local/www/widgets/widgets/rss.widget.php +@@ -25,6 +25,22 @@ require_once("guiconfig.inc"); + require_once("pfsense-utils.inc"); + require_once("functions.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + /* bring in the Composer autoloader */ + require_once('vendor/autoload.php'); + +@@ -32,10 +47,37 @@ use SimplePie\SimplePie; + + if ($_POST['widgetkey']) { + set_customwidgettitle($user_settings); +- $user_settings['widgets'][$_POST['widgetkey']]['rssfeed'] = str_replace("\n", ",", htmlspecialchars($_POST['rssfeed'], ENT_QUOTES | ENT_HTML401)); +- $user_settings['widgets'][$_POST['widgetkey']]['rssmaxitems'] = str_replace("\n", ",", htmlspecialchars($_POST['rssmaxitems'], ENT_QUOTES | ENT_HTML401)); +- $user_settings['widgets'][$_POST['widgetkey']]['rsswidgetheight'] = htmlspecialchars($_POST['rsswidgetheight'], ENT_QUOTES | ENT_HTML401); +- $user_settings['widgets'][$_POST['widgetkey']]['rsswidgettextlength'] = htmlspecialchars($_POST['rsswidgettextlength'], ENT_QUOTES | ENT_HTML401); ++ ++ if ($_POST['rssfeed']) { ++ $validfeeds = []; ++ /* Allow feeds separated by comma or newline */ ++ $feeds = preg_split('/[,\n]/', $_POST['rssfeed']); ++ foreach ($feeds as $feed) { ++ /* Trim any extra whitespace as the submitted value may have \r at the end. */ ++ $feed = trim($feed); ++ if (is_URL($feed)) { ++ $validfeeds[] = $feed; ++ } ++ } ++ $user_settings['widgets'][$_POST['widgetkey']]['rssfeed'] = htmlspecialchars(implode(",", $validfeeds), ENT_QUOTES | ENT_HTML401); ++ } ++ ++ if (is_numeric($_POST['rssmaxitems'])) { ++ $user_settings['widgets'][$_POST['widgetkey']]['rssmaxitems'] = $_POST['rssmaxitems']; ++ } else { ++ unset($user_settings['widgets'][$_POST['widgetkey']]['rssmaxitems']); ++ } ++ if (is_numeric($_POST['rsswidgetheight'])) { ++ $user_settings['widgets'][$_POST['widgetkey']]['rsswidgetheight'] = $_POST['rsswidgetheight']; ++ } else { ++ unset($user_settings['widgets'][$_POST['widgetkey']]['rsswidgetheight']); ++ } ++ if (is_numeric($_POST['rsswidgettextlength'])) { ++ $user_settings['widgets'][$_POST['widgetkey']]['rsswidgettextlength'] = $_POST['rsswidgettextlength']; ++ } else { ++ unset($user_settings['widgets'][$_POST['widgetkey']]['rsswidgettextlength']); ++ } ++ + save_widget_settings($_SESSION['Username'], $user_settings["widgets"], gettext("Saved RSS Widget feed via Dashboard.")); + header("Location: /"); + } +diff --git a/src/usr/local/www/widgets/widgets/services_status.widget.php b/src/usr/local/www/widgets/widgets/services_status.widget.php +index 0e316d77f9241f77865d6284d75dabd0034ddba7..04dbaba8686ef39dea6ad90fccf06959a139c97e 100644 +--- a/src/usr/local/www/widgets/widgets/services_status.widget.php ++++ b/src/usr/local/www/widgets/widgets/services_status.widget.php +@@ -29,6 +29,22 @@ require_once("ipsec.inc"); + require_once("vpn.inc"); + require_once("/usr/local/www/widgets/include/services_status.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $services = get_services(); + + $numsvcs = count($services); +diff --git a/src/usr/local/www/widgets/widgets/smart_status.widget.php b/src/usr/local/www/widgets/widgets/smart_status.widget.php +index e9e2502df474b2d9e7bb8b94af47f2893e580d9f..d97dde69ec14168be34bb0768bb2887d64929957 100644 +--- a/src/usr/local/www/widgets/widgets/smart_status.widget.php ++++ b/src/usr/local/www/widgets/widgets/smart_status.widget.php +@@ -30,6 +30,23 @@ require_once("guiconfig.inc"); + require_once("pfsense-utils.inc"); + require_once("functions.inc"); + require_once("/usr/local/www/widgets/include/smart_status.inc"); ++ ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $specplatform = system_identify_specific_platform(); + + $devs = array(); +diff --git a/src/usr/local/www/widgets/widgets/system_information.widget.php b/src/usr/local/www/widgets/widgets/system_information.widget.php +index cf69ee3b7fb57c32e7bce323740832831ec7628a..bee990a8d586888d5c810463a61813781b17bed6 100644 +--- a/src/usr/local/www/widgets/widgets/system_information.widget.php ++++ b/src/usr/local/www/widgets/widgets/system_information.widget.php +@@ -32,6 +32,22 @@ require_once('notices.inc'); + require_once('system.inc'); + include_once("includes/functions.inc.php"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + $sysinfo_items = array( + 'name' => gettext('Name'), + 'user' => gettext('User'), +diff --git a/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php b/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php +index 0f21af304f83ee1d4f48b7a5150af3d39013b66d..17374449cd2ec8e8203d19fd39aaff4f9d07c87a 100644 +--- a/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php ++++ b/src/usr/local/www/widgets/widgets/thermal_sensors.widget.php +@@ -24,6 +24,21 @@ + require_once("guiconfig.inc"); + require_once("system.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} + + //========================================================================= + //called by showThermalSensorsData() (jQuery Ajax call) in thermal_sensors.js +diff --git a/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php b/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php +index ef3f5d52283eef933b12ea5e15e22b9ab4302cf6..9f045a153b89512df92edc642b6c789d8dcf8402 100644 +--- a/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php ++++ b/src/usr/local/www/widgets/widgets/wake_on_lan.widget.php +@@ -26,6 +26,22 @@ require_once("guiconfig.inc"); + require_once("system.inc"); + require_once("/usr/local/www/widgets/include/wake_on_lan.inc"); + ++/* ++ * Validate the "widgetkey" value. ++ * When this widget is present on the Dashboard, $widgetkey is defined before ++ * the Dashboard includes the widget. During other types of requests, such as ++ * saving settings or AJAX, the value may be set via $_POST or similar. ++ */ ++if ($_POST['widgetkey'] || $_GET['widgetkey']) { ++ $rwidgetkey = isset($_POST['widgetkey']) ? $_POST['widgetkey'] : (isset($_GET['widgetkey']) ? $_GET['widgetkey'] : null); ++ if (is_valid_widgetkey($rwidgetkey, $user_settings, __FILE__)) { ++ $widgetkey = $rwidgetkey; ++ } else { ++ print gettext("Invalid Widget Key"); ++ exit; ++ } ++} ++ + if (isset($config['wol']['wolentry']) && is_array($config['wol']['wolentry'])) { + $wolcomputers = config_get_path('wol/wolentry'); + } else { diff --git a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15856-openvpn-mgmt-fix.patch b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15856-openvpn-mgmt-fix.patch new file mode 100644 index 000000000000..22dcb67bee8f --- /dev/null +++ b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/15856-openvpn-mgmt-fix.patch @@ -0,0 +1,150 @@ +diff --git a/src/etc/inc/openvpn.inc b/src/etc/inc/openvpn.inc +index 5102d6ef5c..384e17356a 100644 +--- a/src/etc/inc/openvpn.inc ++++ b/src/etc/inc/openvpn.inc +@@ -2291,14 +2291,22 @@ function openvpn_get_client_status($client, $socket) { + + function openvpn_kill_client($port, $remipp, $client_id) { + global $g; ++ $killed = -1; + +- //$tcpsrv = "tcp://127.0.0.1:{$port}"; +- $tcpsrv = "unix://{$g['openvpn_base']}/{$port}/sock"; ++ $port = basename($port); ++ $sock_path = "{$g['openvpn_base']}/{$port}/sock"; ++ /* If the socket doesn't exist, or if the remote IP address and port are ++ * not valid, then do not proceed. */ ++ if (!file_exists($sock_path) || ++ !is_ipaddrwithport($remipp)) { ++ return $killed; ++ } ++ $socket = "unix://{$sock_path}"; + $errval = null; + $errstr = null; + + /* open a tcp connection to the management port of each server */ +- $fp = @stream_socket_client($tcpsrv, $errval, $errstr, 1); ++ $fp = @stream_socket_client($socket, $errval, $errstr, 1); + $killed = -1; + if ($fp) { + stream_set_timeout($fp, 1); +diff --git a/src/usr/local/www/status_openvpn.php b/src/usr/local/www/status_openvpn.php +index 1cc6f617e8..daf2fa1123 100644 +--- a/src/usr/local/www/status_openvpn.php ++++ b/src/usr/local/www/status_openvpn.php +@@ -37,13 +37,33 @@ require_once("openvpn.inc"); + require_once("shortcuts.inc"); + require_once("service-utils.inc"); + ++$servers = openvpn_get_active_servers(); ++$sk_servers = openvpn_get_active_servers("p2p"); ++$clients = openvpn_get_active_clients(); ++ + /* Handle AJAX */ + if ($_POST['action']) { + if ($_POST['action'] == "kill") { +- $port = $_POST['port']; +- $remipp = $_POST['remipp']; +- $client_id = $_POST['client_id']; +- if (!empty($port) and !empty($remipp)) { ++ $port = $_POST['port']; ++ $remipp = $_POST['remipp']; ++ $client_id = $_POST['client_id']; ++ $error = false; ++ ++ /* Validate remote IP address and port. */ ++ if (!is_ipaddrwithport($remipp)) { ++ $error = true; ++ } ++ /* Validate submitted server ID */ ++ $found_server = false; ++ foreach ($servers as $server) { ++ if ($port == $server['mgmt']) { ++ $found_server = true; ++ } else { ++ continue; ++ } ++ } ++ ++ if (!$error && $found_server) { + $retval = openvpn_kill_client($port, $remipp, $client_id); + echo htmlentities("|{$port}|{$remipp}|{$retval}|"); + } else { +@@ -64,24 +84,18 @@ if ($_POST['action']) { + } + } + +-$servers = openvpn_get_active_servers(); +-$sk_servers = openvpn_get_active_servers("p2p"); +-$clients = openvpn_get_active_clients(); +- + include("head.inc"); ?> + +
+ + + +- +diff --git a/src/usr/local/www/services_acb_changekey.php b/src/usr/local/www/services_acb_changekey.php +new file mode 100644 +index 0000000000..5c0815600f +--- /dev/null ++++ b/src/usr/local/www/services_acb_changekey.php +@@ -0,0 +1,193 @@ ++. ++ * All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++##|+PRIV ++##|*IDENT=page-services-acb-changekey ++##|*NAME=Services: Auto Config Backup: Change Device Key ++##|*DESCR=Change the auto config backup device key. ++##|*MATCH=services_acb_changekey.php* ++##|-PRIV ++ ++require_once("guiconfig.inc"); ++require_once("functions.inc"); ++require_once("pfsense-utils.inc"); ++require_once("services.inc"); ++require_once("acb.inc"); ++ ++$pconfig = config_get_path('system/acb', []); ++ ++if ($_POST['generatekey']) { ++ print json_encode(['newdevicekey' => acb_generate_device_key()]); ++ exit; ++} ++ ++$userkey = get_acb_device_key(); ++ ++if (isset($_POST['updatekey'])) { ++ unset($input_errors); ++ ++ /* Add validation */ ++ if (!is_valid_acb_device_key($_POST['devkey'])) { ++ $input_errors[] = gettext("Invalid Device Key value."); ++ } ++ ++ if (!$input_errors) { ++ /* Update key */ ++ config_set_path('system/acb/device_key', $_POST['devkey']); ++ write_config(sprintf(gettext('Changed the AutoConfigBackup device key from %s to %s'), ++ $userkey, ++ $_POST['devkey'])); ++ Header("Location: /services_acb_settings.php"); ++ exit; ++ } ++} ++ ++$pgtitle = array(gettext("Services"), gettext("Auto Configuration Backup"), gettext("Change Device Key")); ++include("head.inc"); ++ ++if ($input_errors) { ++ print_input_errors($input_errors); ++} ++ ++$tab_array = array(); ++$tab_array[] = array("Settings", false, "/services_acb_settings.php"); ++$tab_array[] = array("Restore", false, "/services_acb.php"); ++$tab_array[] = array("Backup Now", false, "/services_acb_backup.php"); ++$tab_array[] = array("Change Device Key", true, "/services_acb_changekey.php"); ++display_top_tabs($tab_array); ++ ++$savebutton = new Form_Button( ++ 'updatekey', ++ 'Update Key', ++ null, ++ 'fa-solid fa-save icon-embed-btn' ++); ++$savebutton->addClass('btn-danger')->setAttribute('disabled', true); ++ ++$form = new Form($savebutton); ++$section = new Form_Section('Change Device Key'); ++ ++$legacy_key = get_acb_legacy_device_key(); ++ ++if (!is_valid_acb_device_key($legacy_key) || ++ ($legacy_key != $userkey)) { ++ $device_key_backups = acb_backup_list($userkey); ++ $section->addInput(new Form_Input( ++ 'currentkey', ++ 'Current Device Key', ++ 'text', ++ $userkey ++ ))->setWidth(7)->setReadonly()->setHelp('Unique key which identifies backups associated with this device.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this device key will be lost!%1$s%1$s' . ++ 'Hosted backups for this device key: %5$d', ++ '
', '', '', acb_key_download_link('device', $userkey), count($device_key_backups)); ++} ++ ++if (is_valid_acb_device_key($legacy_key)) { ++ $legacy_key_backups = acb_backup_list($legacy_key); ++ $section->addInput(new Form_Input( ++ 'legacykey', ++ 'Legacy Device Key', ++ 'text', ++ $legacy_key ++ ))->setWidth(7)->setReadonly()->setHelp('Unique key which identifies backups associated with this device.%1$s%1$s' . ++ 'This is a legacy style key derived from the SSH public key.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this legacy device key will be lost!%1$s%1$s' . ++ 'Hosted backups for this legacy key: %5$d', ++ '
', '', '', acb_key_download_link('legacy', $userkey), count($legacy_key_backups)); ++} ++ ++$group = new Form_Group("New Device Key"); ++ ++$group->add(new Form_Input( ++ 'devkey', ++ 'Device Key', ++ 'text', ++ "" ++))->setWidth(7)->setHelp('New device key, replaces the Current Device Key.%1$s%1$s' . ++ 'Use the %2$sGenerate New Key%3$s button to create a new randomized key in the proper format, or ' . ++ 'paste a properly formatted key into the field. Keys must be 64 character hexadecimal strings (0-9, a-f).%1$s%1$s' . ++ '%2$sTreat this key as a secret!%3$s%1$sAnyone who has this key can manipulate the backups for this key.%1$s%1$s' . ++ '%2$sChanging the Device Key removes the existing device key!%3$s%1$sUse the download icon above to save a copy ' . ++ 'of the old key before continuing!', ++ '
', '', ''); ++ ++$group->add(new Form_Button( ++ 'generatekey', ++ 'Generate New Key', ++ null, ++ 'fa-solid fa-arrows-rotate' ++))->addClass('btn-info btn-xs'); ++ ++$section->add($group); ++ ++$section->addInput(new Form_Checkbox( ++ 'confirmation', ++ 'Warning', ++ 'Check this box to acknowledge that the Device Key will change, disconnecting from previous backups', ++ false ++))->setHelp('Checking this box enables the Update Key button. ' . ++ 'Save a copy of the old Device Key before continuing.%1$s%1$s' . ++ 'Old backups are not automatically removed from the server and must be removed manually.', ++ '
'); ++ ++$form->add($section); ++ ++print $form; ++ ++?> ++
++ ++ ++ ++ +diff --git a/src/usr/local/www/services_acb_settings.php b/src/usr/local/www/services_acb_settings.php +index beb283f0d2..e5e459bfae 100644 +--- a/src/usr/local/www/services_acb_settings.php ++++ b/src/usr/local/www/services_acb_settings.php +@@ -1,6 +1,6 @@ + (int)"50" ) { +- $input_errors[] = gettext("You may not retain more than 50 manual backups."); ++ /* Check Hint/Identifier */ ++ if (!empty($_POST['hint']) && ++ (strlen($_POST['hint']) > 255)) { ++ $input_errors[] = gettext("Hint/Identifier must be less than 255 characters in length."); + } + +- $pwd = ""; ++ /* Check Manual backup limit */ ++ if (!empty($_POST['numman']) && ++ !is_numericint($_POST['numman'])) { ++ $input_errors[] = gettext("Manual Backup Limit must be blank or an integer."); ++ } ++ if ((int)$_POST['numman'] < 0) { ++ $input_errors[] = gettext("Manual Backup Limit cannot be negative."); ++ } ++ if ((int)$_POST['numman'] > 50) { ++ $input_errors[] = gettext("Manual Backup Limit cannot be larger than 50."); ++ } + +- if (!$input_errors) { +- if($update_ep) { +- $pwd = $pconfig['encryption_password']; +- } ++ /* Check Descending Date Order */ ++ if (!empty($_POST['reverse']) && ++ ($_POST['reverse'] != 'yes')) { ++ $input_errors[] = gettext("Invalid Descending Date Order value."); ++ } + ++ /* Store updated ACB configuration */ ++ if (!$input_errors) { + $pconfig = setup_ACB( + $pconfig['enable'], + $pconfig['hint'], +@@ -97,7 +116,7 @@ if (isset($_POST['save'])) { + $pconfig['dow'], + $pconfig['numman'], + $pconfig['reverse'], +- $pwd ++ ($update_ep ? $pconfig['encryption_password'] : "") + ); + } + } +@@ -112,7 +131,7 @@ if ($input_errors) { + $tab_array = array(); + $tab_array[] = array("Settings", true, "/services_acb_settings.php"); + $tab_array[] = array("Restore", false, "/services_acb.php"); +-$tab_array[] = array("Backup now", false, "/services_acb_backup.php"); ++$tab_array[] = array("Backup Now", false, "/services_acb_backup.php"); + display_top_tabs($tab_array); + + $form = new Form; +@@ -185,37 +204,75 @@ $group->add(new Form_Input( + ))->setHelp("Day of week (0-6)"); + + $group->addClass("cronsched"); +-$group->setHelp(sprintf('Use * ("every"), divisor or exact value. Minutes are randomly chosen by default. See %s for more information.', ++$group->setHelp(sprintf('Use * ("every"), divisor, or exact value. Minutes are randomly chosen by default. See %s for more information.', + 'Cron format')); + $section->add($group); + ++$group = new Form_Group("Device Key"); ++ ++$userkey = get_acb_device_key(); ++$legacy_key = get_acb_legacy_device_key(); ++$legacy_string = ""; ++if (is_valid_acb_device_key($legacy_key) && ++ ($legacy_key == $userkey)) { ++ $legacy_string = sprintf(gettext('%1$s%1$sThis is a legacy style key derived from the SSH public key.%1$s' . ++ 'The best practice is to change this key to a randomized key using the ' . ++ '%2$sChange Key%3$s button.'), ++ '
', '', ''); ++} ++ ++$group->add(new Form_Input( ++ 'devkey', ++ 'Device Key', ++ 'text', ++ $userkey ++))->setWidth(7)->setReadonly()->setHelp('Unique key which identifies backups associated with this device.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this device will be lost!%5$s', ++ '
', '', '', acb_key_download_link('device', $userkey), $legacy_string); ++ ++$group->add(new Form_Button( ++ 'changekey', ++ 'Change Key', ++ null, ++ 'fa-solid fa-key' ++))->addClass('btn-info btn-xs'); ++ ++$section->add($group); ++ + $section->addPassword(new Form_Input( + 'encryption_password', + '*Encryption Password', + 'password', + $pconfig['encryption_password'] +-))->setHelp("The best practice for security is to use a long and complex password."); ++))->setHelp('AutoConfigBackup uses this string to encrypt the contents of the backup before upload. ' . ++ 'The best practice for security is to use a long and complex string.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s%1$s' . ++ 'If this password is lost, any backups encrypted with this string will be unreadable!', ++ '
', '', ''); + + $section->addInput(new Form_Input( + 'hint', + 'Hint/Identifier', + 'text', + $pconfig['hint'] +-))->setHelp("You may optionally provide an identifier which will be stored in plain text along with each encrypted backup. " . +- "This may allow the Netgate support team to locate your key should you lose it."); ++))->setHelp("Optional identifier which AutoConfigBackup will store in plain text along with each encrypted backup. " . ++ "This may allow Netgate TAC to recover a device key should it become lost."); + + $section->addInput(new Form_Input( + 'numman', +- 'Manual backups to keep', ++ 'Manual Backup Limit', + 'number', +- $pconfig['numman'] +-))->setHelp("It may be useful to specify how many manual backups are retained on the server so that automatic backups do not overwrite them." . +- "A maximum of 50 retained manual backups (of the 100 total backups) is permitted."); ++ $pconfig['numman'], ++ ['min'=>'0', 'max'=>'50'] ++))->setHelp("Number of manual backup entries AutoConfigBackup will retain on the server, " . ++ "which will not be overwritten by automatic backups. " . ++ "The maximum value is 50 retained manual backups out of the 100 total entries."); + + $section->addInput(new Form_Checkbox( + 'reverse', +- 'Descending Order by Date', +- 'List backups in descending order', ++ 'Descending Date Order', ++ 'List backups in descending order by revision date/time', + ($pconfig['reverse'] == "yes") + ))->setHelp("List backups in descending order (newest first) when viewing the restore section."); + +@@ -228,13 +285,19 @@ print $form; + + + diff --git a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/acb-update-ce-2.7.2.patch b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/acb-update-ce-2.7.2.patch new file mode 100644 index 000000000000..8954b27dec76 --- /dev/null +++ b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/acb-update-ce-2.7.2.patch @@ -0,0 +1,2288 @@ +diff -rub --new-file old/src/etc/inc/acb.inc new/src/etc/inc/acb.inc +--- old/src/etc/inc/acb.inc 2025-01-24 13:41:54.897324247 -0500 ++++ new/src/etc/inc/acb.inc 2025-01-27 10:58:06.363105561 -0500 +@@ -5,7 +5,7 @@ + * part of pfSense (https://www.pfsense.org) + * Copyright (c) 2008-2013 BSD Perimeter + * Copyright (c) 2013-2016 Electric Sheep Fencing +- * Copyright (c) 2014-2023 Rubicon Communications, LLC (Netgate) ++ * Copyright (c) 2014-2025 Rubicon Communications, LLC (Netgate) + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); +@@ -21,235 +21,271 @@ + * limitations under the License. + */ + ++require_once("config.inc"); + require_once("filter.inc"); + require_once("notices.inc"); + +-// If there is no ssh key in the system to identify this firewall, generate a pair now +-function get_device_key() { +- if (!file_exists("/etc/ssh/ssh_host_ed25519_key.pub")) { +- // Remove any possible matching key so we don't get an overwrite prompt +- if (file_exists('/etc/ssh/ssh_host_ed25519_key')) { +- unlink('/etc/ssh/ssh_host_ed25519_key'); +- } +- +- // Generate a new key pair +- exec("/usr/bin/nice -n20 /usr/bin/ssh-keygen -t ed25519 -b 4096 -N '' -f /etc/ssh/ssh_host_ed25519_key"); +- sleep(2); +- +- $cnt = 0; +- while(!file_exists("/etc/ssh/ssh_host_ed25519_key.pub")) { +- sleep(2); +- if (++$cnt > 10) { +- return ""; +- } +- } +- } ++global $acb_base_url; ++$acb_base_url = "https://acb.netgate.com"; + +- $pkey = file_get_contents("/etc/ssh/ssh_host_ed25519_key.pub"); ++global $acb_last_backup_file; ++$acb_last_backup_file = "/cf/conf/lastACBentry.txt"; + +- // Check that the key looks reasonable +- if (substr($pkey, 0, 3) != "ssh") { +- return ""; +- } ++global $acb_force_file; ++$acb_force_file = "/tmp/forceacb"; + +- return hash("sha256", $pkey); ++/* Set up time zones for conversion. See #5250 */ ++global $acb_server_tz; ++$acb_server_tz = new DateTimeZone('America/Chicago'); ++ ++/* Backup reason strings for which ACB will not create remote backup entries */ ++global $acb_ignore_reasons; ++$acb_ignore_reasons = [ ++ 'snort', ++ 'pfblocker', ++ 'minicron', ++ 'merged in config', ++ 'intermediate config write during package', ++ 'acbupload.php', ++ 'execacb.php' ++]; ++ ++/* Check a string to determine if it is a valid device key */ ++function is_valid_acb_device_key($dk) { ++ $dk = trim($dk); ++ if (!is_null($dk) && ++ !empty($dk) && ++ (strlen($dk) == 64) && ++ ctype_xdigit($dk)) { ++ return true; ++ } ++ return false; + } + +-$origkey = get_device_key(); +- +-if (isset($_REQUEST['userkey']) && !empty($_REQUEST['userkey'])) { +- $userkey = htmlentities($_REQUEST['userkey']); +-} else { +- $userkey = get_device_key(); ++/* Check a string to determine if it is a valid revision identifier */ ++function is_valid_acb_revision($revision) { ++ /* The revision must be a valid date string */ ++ /* Ensure this returns boolean true/false not a timestamp when true */ ++ return (strtotime($revision) !== false); + } + +-$uniqueID = system_get_uniqueid(); +- +-/* Check whether ACB is enabled */ +-function acb_enabled() { +- global $config; +- $acb_enabled = false; +- +- if (is_array($config['system']['acb'])) { +- if ($config['system']['acb']['enable'] == "yes") { +- $acb_enabled = true; ++/* Check if a reason string should be ignored by ACB. */ ++function is_acb_ignored_reason($reason) { ++ global $acb_ignore_reasons; ++ foreach ($acb_ignore_reasons as $term) { ++ if (stripos($reason, $term) !== false) { ++ return true; + } + } +- +- return $acb_enabled; ++ return false; + } + +-// Ensures patches match +-function acb_custom_php_validation_command($post, &$input_errors) { +- global $_POST, $savemsg, $config; ++/* Generate a new random device key */ ++function acb_generate_device_key() { ++ $keyoutput = ""; ++ $keystatus = ""; ++ exec("/bin/dd status=none if=/dev/random bs=4096 count=1 | /usr/bin/openssl sha256 | /usr/bin/cut -f2 -d' '", $keyoutput, $keystatus); ++ if (($keystatus == 0) && ++ is_array($keyoutput)) { ++ $keyoutput = trim($keyoutput[0]); + +- // Do nothing when ACB is disabled in configuration +- // This also makes it possible to delete the credentials from config.xml +- if (!acb_enabled()) { +- // We do not need to store this value. +- unset($_POST['testconnection']); +- return; ++ if (is_valid_acb_device_key($keyoutput)) { ++ return $keyoutput; + } +- +- if (!$post['crypto_password'] or !$post['crypto_password2']) { +- $input_errors[] = "The encryption password is required."; +- } +- +- if ($post['crypto_password'] <> $post['crypto_password2']) { +- $input_errors[] = "Sorry, the entered encryption passwords do not match."; + } ++ return null; ++} + +- if ($post['testconnection']) { +- $status = test_connection($post); +- if ($status) { +- $savemsg = "Connection to the ACB server was tested with no errors."; ++/* Locate a legacy ACB key for a device, which is derived from the SSH key */ ++function get_acb_legacy_device_key() { ++ if (file_exists('/etc/ssh/ssh_host_ed25519_key.pub')) { ++ $pkey = file_get_contents("/etc/ssh/ssh_host_ed25519_key.pub"); ++ // Check that the SSH key looks reasonable ++ if (substr($pkey, 0, 3) == "ssh") { ++ return hash("sha256", $pkey); + } + } +- +- // We do not need to store this value. +- unset($_POST['testconnection']); ++ return null; + } + +-function acb_custom_php_resync_config_command() { +- // Do nothing when ACB is disabled in configuration +- if (!acb_enabled()) { +- return; ++/* Locate and return the ACB device key for this installation. If there is no ++ * viable key, generate and store a new key. */ ++function get_acb_device_key() { ++ $config_device_key = config_get_path('system/acb/device_key'); ++ ++ /* If there is no device key in the configuration, check for a legacy key */ ++ if (!is_valid_acb_device_key($config_device_key) && ++ acb_enabled() && ++ file_exists('/etc/ssh/ssh_host_ed25519_key.pub')) { ++ $config_device_key = get_acb_legacy_device_key(); ++ } ++ ++ /* Still no key, so generate a new random key */ ++ if (!is_valid_acb_device_key($config_device_key)) { ++ $config_device_key = acb_generate_device_key(); ++ /* Only store the key if it's valid */ ++ if (is_valid_acb_device_key($config_device_key)) { ++ config_set_path('system/acb/device_key', $config_device_key); ++ write_config(gettext('Generated new randomized AutoConfigBackup device key')); + } +- +- unlink_if_exists("/cf/conf/lastpfSbackup.txt"); +- +- if (!function_exists("filter_configure")) { +- require_once("filter.inc"); + } + +- filter_configure(); +- +- if ($savemsg) { +- $savemsg .= "
"; ++ /* Still no valid key, something went wrong */ ++ if (!is_valid_acb_device_key($config_device_key)) { ++ log_error(gettext('Unable to locate or generate a valid AutoConfigBackup device key')); ++ return null; ++ } else { ++ return $config_device_key; + } +- +- $savemsg .= "A configuration backup has been queued."; + } + +-function test_connection($post) { +- global $savemsg, $config, $g; ++/* Check whether ACB is enabled */ ++function acb_enabled() { ++ return (config_get_path('system/acb/enable', '') == "yes"); ++} + +- // Do nothing when booting or when not enabled +- if (platform_booting() || !acb_enabled()) { +- return; ++/* Check if this device can resolve the ACB hostname via DNS. */ ++function acb_check_dns() { ++ global $acb_base_url; ++ if (!check_dnsavailable(parse_url($acb_base_url, PHP_URL_HOST))) { ++ acb_error_log(sprintf(gettext('Unable to resolve %s'), ++ parse_url($acb_base_url, PHP_URL_HOST))); ++ return false; ++ } else { ++ return true; + } ++} + +- // Separator used during client / server communications +- $oper_sep = "\|\|"; +- +- // Encryption password +- $decrypt_password = $post['crypto_password']; +- +- // Defined username. Username must be sent lowercase. See Redmine #7127 and Netgate Redmine #163 +- $username = strtolower($post['username']); +- +- // Defined password +- $password = $post['password']; +- +- // Set hostname +- $hostname = $config['system']['hostname'] . "." . $config['system']['domain']; ++/* Change the time zone to reflect local time of ACB revisions. ++ * See Redmine #5250 */ ++function acb_time_shift($revision, $format = DATE_RFC2822) { ++ global $acbtz; ++ $budate = new DateTime($revision, $acbtz); ++ $mytz = new DateTimeZone(date_default_timezone_get()); ++ $budate->setTimezone($mytz); ++ return htmlspecialchars($budate->format($format)); ++} + +- // URL to restore.php +- $get_url = "https://acb.netgate.com/getbkp"; ++/* ++ * Query the ACB server via cURL and return the data ++ * ++ * Parameters: ++ * endpoint: ++ * Relative URL endpoint on the ACB service, not including the base ++ * hostname. ++ * postfields: ++ * Array containing post fields and their values to submit. ++ * multipart: ++ * True when submitting multi-part form data (e.g. save/upload) ++ * ++ * Returns: ++ * data: ++ * Content returned from the server ++ * httpcode: ++ * HTTP code returned by the server ++ */ ++function acb_query_service($endpoint, $post_fields, $multipart = false) { ++ global $acb_base_url; ++ $url = "{$acb_base_url}/{$endpoint}"; ++ ++ /* Bail if passed invalid data */ ++ if (empty($endpoint) || ++ empty($post_fields) || ++ !is_array($post_fields)) { ++ return [null, null, 1]; ++ } ++ ++ /* Add UID */ ++ $post_fields['uid'] = system_get_uniqueid(); ++ ++ /* Store this now as it may be lost in the next step. */ ++ $post_fields_count = count($post_fields); ++ if (!$multipart) { ++ $post_fields = http_build_query($post_fields); ++ } + +- // Populate available backups + $curl_session = curl_init(); +- curl_setopt($curl_session, CURLOPT_URL, $get_url); +- curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($curl_session, CURLOPT_POST, 1); ++ curl_setopt($curl_session, CURLOPT_URL, $url); ++ curl_setopt($curl_session, CURLOPT_POST, $post_fields_count); ++ curl_setopt($curl_session, CURLOPT_POSTFIELDS, $post_fields); + curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, 1); ++ curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($curl_session, CURLOPT_CONNECTTIMEOUT, 55); + curl_setopt($curl_session, CURLOPT_TIMEOUT, 30); + curl_setopt($curl_session, CURLOPT_USERAGENT, g_get('product_label') . '/' . rtrim(file_get_contents("/etc/version"))); + // Proxy + set_curlproxy($curl_session); + +- curl_setopt($curl_session, CURLOPT_POSTFIELDS, "action=showbackups&hostname={$hostname}"); + $data = curl_exec($curl_session); ++ $httpcode = curl_getinfo($curl_session, CURLINFO_RESPONSE_CODE); ++ $errno = curl_errno($curl_session); + +- if (curl_errno($curl_session)) { +- return("An error occurred " . curl_error($curl_session)); ++ if ($errno) { ++ $fd = fopen("/tmp/acb_debug.txt", "w"); ++ fwrite($fd, $url . "\n\n"); ++ fwrite($fd, var_export($post_fields, true)); ++ fwrite($fd, $data); ++ fwrite($fd, curl_error($curl_session)); ++ fclose($fd); + } else { + curl_close($curl_session); + } +- +- return; ++ return [$data, $httpcode, $errno]; + } + +-function upload_config($manual = false) { +- global $config, $g, $input_errors, $userkey, $uniqueID; +- +- if (empty($userkey)) { +- $userkey = get_device_key(); +- } ++/* Check if a backup is necessary (has config changed since last upload) */ ++function is_acb_upload_needed() { ++ global $acb_last_backup_file; + +- if (empty($uniqueID)) { +- $uniqueID = system_get_uniqueid(); ++ if (file_exists($acb_last_backup_file)) { ++ $last_backup_date = trim(file_get_contents($acb_last_backup_file)); ++ } else { ++ $last_backup_date = ""; + } + +- // Do nothing when booting or when not enabled +- if (platform_booting() || !acb_enabled()) { +- return; +- } ++ return ($last_backup_date <> config_get_path('revision/time')); ++} + +- /* +- * pfSense upload config to acb.netgate.com script +- * This file plugs into config.inc (/usr/local/pkg/parse_config) +- * and runs every time the running firewall filter changes. +- * ++/* Stage a config backup for uploading which will be picked up later by the ++ * acbupload.php cron job which performs the actual upload process. + */ ++function acb_backup_stage_upload($manual = false) { ++ global $acb_base_url; + +- if (file_exists("/tmp/acb_nooverwrite")) { +- unlink("/tmp/acb_nooverwrite"); +- $nooverwrite = "true"; +- } else { +- $nooverwrite = "false"; +- } +- +- // Define some needed variables +- if (file_exists("/cf/conf/lastpfSbackup.txt")) { +- $last_backup_date = str_replace("\n", "", file_get_contents("/cf/conf/lastpfSbackup.txt")); +- } else { +- $last_backup_date = ""; ++ /* Do nothing when booting or when not enabled */ ++ if (is_platform_booting() || ++ !acb_enabled()) { ++ return; + } + +- $last_config_change = config_get_path('revision/time'); +- $hostname = $config['system']['hostname'] . "." . $config['system']['domain']; ++ /* Define required variables */ ++ $userkey = get_acb_device_key(); ++ $hostname = config_get_path('system/hostname') . "." . config_get_path('system/domain'); + $reason = config_get_path('revision/description'); +- +- if ($manual && array_key_exists('numman', $config['system']['acb'])) { +- $manmax = config_get_path('system/acb/numman'); +- } else { +- $manmax = "0"; +- } +- ++ $manmax = config_get_path('system/acb/numman', '0'); + $encryptpw = config_get_path('system/acb/encryption_password'); + +- // Define upload_url, must be present after other variable definitions due to username, password +- $upload_url = "https://acb.netgate.com/save"; ++ if (is_acb_ignored_reason($reason)) { ++ log_error(sprintf(gettext('Skipping staging AutoConfigBackup entry for ignored reason: %s.'), $reason)); ++ return; ++ } + + if (!$encryptpw) { + if (!file_exists("/cf/conf/autoconfigback.notice")) { +- $notice_text = "The encryption password is not set for Automatic Configuration Backup."; +- $notice_text .= " Please correct this in Services -> AutoConfigBackup -> Settings."; ++ $notice_text = gettext('The Automatic Configuration Backup Encryption Password is not set. ' . ++ 'Configure the Encryption Password at Services > AutoConfigBackup > Settings.'); + log_error($notice_text); + file_notice("AutoConfigBackup", $notice_text, $notice_text, ""); + touch("/cf/conf/autoconfigback.notice"); + } + } else { +- /* If configuration has changed, upload to pfS */ +- if ($last_backup_date <> $last_config_change) { +- +- $notice_text = "Beginning configuration backup to " . $upload_url; ++ /* If the configuration has changed, upload to ACB service */ ++ if (is_acb_upload_needed()) { ++ $notice_text = sprintf(gettext('Staging AutoConfigBackup encrypted configuration backup for deferred upload to %s'), $acb_base_url); + log_error($notice_text); + update_filter_reload_status($notice_text); + +- // Encrypt config.xml ++ /* Encrypt config.xml contents */ + $data = file_get_contents("/cf/conf/config.xml"); + $raw_config_sha256_hash = trim(shell_exec("/sbin/sha256 /cf/conf/config.xml | /usr/bin/awk '{ print $4 }'")); + $data = encrypt_data($data, $encryptpw); +@@ -257,112 +293,224 @@ + tagfile_reformat($data, $data, "config.xml"); + file_put_contents($tmpname, $data); + ++ /* Define backup metadata */ + $post_fields = array( +- 'reason' => htmlspecialchars($reason), +- 'uid' => $uniqueID, ++ 'reason' => substr(htmlspecialchars($reason), 0, 1024), + 'file' => curl_file_create($tmpname, 'image/jpg', 'config.jpg'), + 'userkey' => htmlspecialchars($userkey), + 'sha256_hash' => $raw_config_sha256_hash, + 'version' => g_get('product_version'), +- 'hint' => $config['system']['acb']['hint'], +- 'manmax' => $manmax ++ 'hint' => substr(config_get_path('system/acb/hint'), 0, 255), ++ 'manmax' => (int)$manmax + ); + + unlink_if_exists($tmpname); + +- if (!is_dir(g_get('acbbackuppath'))) { +- mkdir(g_get('acbbackuppath')); +- } ++ /* Location to stage backup file pairs */ ++ $acbuploadpath = g_get('acbbackuppath'); + +- file_put_contents(g_get('acbbackuppath') . $post_fields['sha256_hash'] . ".form", json_encode($post_fields)); +- file_put_contents(g_get('acbbackuppath') . $post_fields['sha256_hash'] . ".data", $data); ++ if (!is_dir($acbuploadpath)) { ++ mkdir($acbuploadpath); ++ } + +- /* +- This functionality is now provided by a cron job /usr/local/sbin/acbupload.php run once per minute when ACB is enabled. +- Commented block can be removed after testing. ++ file_put_contents($acbuploadpath . $post_fields['sha256_hash'] . ".form", json_encode($post_fields)); ++ file_put_contents($acbuploadpath . $post_fields['sha256_hash'] . ".data", $data); ++ } else { ++ /* Debugging */ ++ //log_error(gettext('No AutoConfigBackup action required.')); ++ } ++ } ++} + +- // Check configuration into the ESF repo +- $curl_session = curl_init(); ++/* Upload all backup entries staged by acb_backup_stage_upload(). */ ++function acb_backup_upload($basename) { ++ global $acb_base_url, $acb_last_backup_file; ++ ++ /* Location of staged backup file pairs */ ++ $acbuploadpath = g_get('acbbackuppath'); ++ ++ /* If the ACB service cannot be resolved, remove staged backup files ++ * and exit. ++ * The check function logs an error, no need to log an error manually. ++ */ ++ if (!acb_check_dns()) { ++ unlink_if_exists($acbuploadpath . $basename . ".data"); ++ unlink_if_exists($acbuploadpath . $basename . ".form"); ++ return; ++ } + +- curl_setopt($curl_session, CURLOPT_URL, "https://acb.netgate.com/save"); +- curl_setopt($curl_session, CURLOPT_POST, count($post_fields)); +- curl_setopt($curl_session, CURLOPT_POSTFIELDS, $post_fields); +- curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, 1); +- curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($curl_session, CURLOPT_CONNECTTIMEOUT, 55); +- curl_setopt($curl_session, CURLOPT_TIMEOUT, 30); +- curl_setopt($curl_session, CURLOPT_USERAGENT, $g['product_label'] . '/' . rtrim(file_get_contents("/etc/version"))); +- // Proxy +- set_curlproxy($curl_session); ++ /* Read the staged form file containing backup metadata */ ++ $formdata = file_get_contents($acbuploadpath . $basename . ".form"); ++ $post_fields = json_decode($formdata, true); ++ ++ /* Check backup reason in metadata against ignore list */ ++ if (is_acb_ignored_reason($post_fields['reason'])) { ++ log_error(sprintf(gettext('Skipping staged AutoConfigBackup entry for ignored reason: %s.'), $post_fields['reason'])); ++ /* Delete the staged backup files */ ++ unlink_if_exists($acbuploadpath . $basename . ".data"); ++ unlink_if_exists($acbuploadpath . $basename . ".form"); ++ return; ++ } + +- $data = curl_exec($curl_session); ++ /* Add the encrytped backup data */ ++ $post_fields['file'] = curl_file_create($acbuploadpath . $basename . ".data", 'image/jpg', 'config.jpg'); + +- unlink_if_exists($tmpname); ++ /* Upload encrypted backup entry and its metadata to the ACB service */ ++ [$data, $httpcode, $errno] = acb_query_service("save", $post_fields, true); + +- if (curl_errno($curl_session)) { +- $fd = fopen("/tmp/backupdebug.txt", "w"); +- fwrite($fd, $upload_url . "" . $fields_string . "\n\n"); +- fwrite($fd, $data); +- fwrite($fd, curl_error($curl_session)); +- fclose($fd); ++ /* Delete the staged backup files no matter the outcome */ ++ unlink_if_exists($acbuploadpath . $basename . ".data"); ++ unlink_if_exists($acbuploadpath . $basename . ".form"); ++ ++ if (strpos(strval($httpcode), '20') === false) { ++ if (empty($data) && $errno) { ++ $data = $errno; + } else { +- curl_close($curl_session); ++ $data = "Unknown error"; + } +- +- if (strpos($data, "500") != false) { +- $notice_text = sprintf(gettext( +- "An error occurred while uploading your %s configuration to "), $g['product_label']) . +- $upload_url . " (" . htmlspecialchars($data) . ")"; +- log_error($notice_text . " - " . $data); +- file_notice("AutoConfigBackup", $notice_text); +- update_filter_reload_status($notice_text); +- $input_errors["acb_upload"] = $notice_text; ++ acb_error_log($data); + } else { +- // Update last pfS backup time +- $fd = fopen("/cf/conf/lastpfSbackup.txt", "w"); +- fwrite($fd, $config['revision']['time']); ++ /* Update last ACB backup time */ ++ $fd = fopen($acb_last_backup_file, "w"); ++ fwrite($fd, config_get_path('revision/time')); + fclose($fd); +- $notice_text = "End of configuration backup to " . $upload_url . " (success)."; ++ $notice_text = sprintf(gettext('Completed AutoConfigBackup encrypted configuration backup upload to %s (success)'), $acb_base_url); + log_error($notice_text); + update_filter_reload_status($notice_text); + } +-*/ ++} ++ ++/* Get a specific backup entry from the ACB service */ ++function acb_backup_get($userkey, $revision) { ++ $post_fields = [ ++ 'userkey' => $userkey, ++ 'revision' => $revision, ++ 'version' => g_get('product_version'), ++ ]; ++ return acb_query_service("getbkp", $post_fields); ++} ++ ++/* Get metadata for a specific backup entry from the list as this ++ * metadata is not included when using the getbkp endpoint. ++ */ ++function acb_backup_get_metadata($userkey, $revision) { ++ /* Reverse the list since the ACB server getbkp returns last match, ++ * otherwise if two entries have the same revision the metadata will ++ * not match. */ ++ $backups = array_reverse(acb_backup_list($userkey)); ++ foreach ($backups as $b) { ++ if ($b['time'] == $revision) { ++ return $b; ++ } ++ } ++ return []; ++} ++ ++/* Decrypt the configuration data from an ACB service backup entry. */ ++function acb_backup_decrypt($data, $password) { ++ $errors = []; ++ ++ $data_split = explode('++++', $data); ++ $sha256 = trim($data_split[0]); ++ $encrypted = $data_split[1]; ++ ++ if (!tagfile_deformat($encrypted, $encrypted, "config.xml")) { ++ $errors[] = gettext('The fetched backup entry does not appear to contain an encrypted configuration.'); ++ } ++ $decrypted = decrypt_data($encrypted, $password); ++ if (!strstr($decrypted, "pfsense") || ++ (strlen($decrypted) < 50)) { ++ $errors[] = gettext('Could not decrypt the fetched configuration backup entry. Check the encryption key and try again.'); + } else { +- // Debugging +- //log_error("No https://acb.netgate.com backup required."); ++ $pos = stripos($decrypted, ""); ++ $decrypted = substr($decrypted, 0, $pos); ++ $decrypted .= "\n"; ++ } ++ ++ return [$decrypted, $encrypted, $sha256, $errors]; ++} ++ ++/* Fetch a list of backups stored for a given device key on the ACB service */ ++function acb_backup_list($userkey) { ++ /* Separator used during client / server communications */ ++ $oper_sep = "\|\|"; ++ ++ $backups = []; ++ ++ $post_fields = [ ++ 'userkey' => $userkey, ++ 'version' => g_get('product_version') ++ ]; ++ /* Fetch backup data for this device key from the ACB service */ ++ [$data, $httpcode, $errno] = acb_query_service("list", $post_fields); ++ ++ /* Loop through fetched data and create a backup list */ ++ foreach (explode("\n", $data) as $ds) { ++ $ds_split = []; ++ preg_match("/^(.*?){$oper_sep}(.*){$oper_sep}(.*)/", $ds, $ds_split); ++ ++ $tmp_array = [ ++ 'username' => $ds_split[1], ++ 'reason' => $ds_split[2], ++ 'time' => $ds_split[3], ++ 'localtime' => acb_time_shift($ds_split[3]) ++ ]; ++ ++ if ($ds_split[3] && $ds_split[1]) { ++ $backups[] = $tmp_array; + } + } ++ ++ return $backups; + } + +-// Save the updated ACB configuration +-// Create a crontab entry for scheduled backups +-// if frequency == "cron", a new crontab entry is created, otherwise any existing +-// ACB entry is removed +-function setup_ACB($enable, $hint, $frequency, $minute, $hours, $month, $day, $dow, $numman, $reverse, $pwd) { +- global $config; ++/* Delete a specific backup entry from the ACB service */ ++function acb_backup_delete($userkey, $revision) { ++ global $acb_base_url; ++ ++ $savemsg = ""; ++ ++ $post_fields = [ ++ 'userkey' => $userkey, ++ 'revision' => $revision, ++ 'version' => g_get('product_version'), ++ ]; ++ [$data, $httpcode, $errno] = acb_query_service("rmbkp", $post_fields); + +- init_config_arr(array('system', 'acb')); ++ if ($errno) { ++ $savemsg = sprintf(gettext('An error occurred while trying to remove the backup revision from %s'), $acb_base_url); ++ } else { ++ $savemsg = sprintf(gettext('Backup revision %s has been removed.'), acb_time_shift($revision)); ++ } ++ return $savemsg; ++} + +- // Randomize the minutes if not specified ++/* Save the ACB configuration. ++ * Creates or removes ACB crontab entry for scheduled backups when necessary. ++ */ ++function setup_ACB($enable, $hint, $frequency, $minute, $hours, $month, $day, $dow, $numman, $reverse, $pwd) { ++ /* Randomize the minutes if not specified */ + if (!isset($minute) || strlen($minute) == 0 || $minute == "0") { + $minute = rand(1, 59); + } + +- $config['system']['acb']['enable'] = $enable; +- $config['system']['acb']['hint'] = $hint; +- $config['system']['acb']['frequency'] = $frequency; +- $config['system']['acb']['minute'] = $minute; +- $config['system']['acb']['hour'] = $hours; +- $config['system']['acb']['month'] = $month; +- $config['system']['acb']['day'] = $day; +- $config['system']['acb']['dow'] = $dow; +- $config['system']['acb']['numman'] = $numman; +- $config['system']['acb']['reverse'] = $reverse; ++ config_set_path('system/acb/enable', $enable); ++ config_set_path('system/acb/hint', $hint); ++ config_set_path('system/acb/frequency', $frequency); ++ config_set_path('system/acb/minute', $minute); ++ config_set_path('system/acb/hour', $hours); ++ config_set_path('system/acb/month', $month); ++ config_set_path('system/acb/day', $day); ++ config_set_path('system/acb/dow', $dow); ++ config_set_path('system/acb/numman', $numman); ++ config_set_path('system/acb/reverse', $reverse); + if (strlen($pwd) >= 8) { +- $config['system']['acb']['encryption_password'] = $pwd; ++ config_set_path('system/acb/encryption_password', $pwd); + } + +- install_cron_job("/usr/bin/nice -n20 /usr/local/bin/php /usr/local/sbin/execacb.php", $frequency == "cron", ++ /* Install or remove cron job for scheduled periodic backups. */ ++ install_cron_job("/usr/bin/nice -n20 /usr/local/bin/php /usr/local/sbin/execacb.php", ++ ($frequency == "cron"), + $minute, + is_numeric($hours) ? $hours : "*", + is_numeric($day) ? $day : "*", +@@ -370,10 +518,40 @@ + is_numeric($dow) ? $dow : "*" + ); + +- // Install cron job +- install_cron_job("/usr/bin/nice -n20 /usr/local/bin/php /usr/local/sbin/acbupload.php", $enable == "yes", "*"); ++ /* Install or remove cron job for uploading staged backups */ ++ install_cron_job("/usr/bin/nice -n20 /usr/local/bin/php /usr/local/sbin/acbupload.php", ++ ($enable == "yes"), ++ "*"); + + write_config("AutoConfigBackup settings updated"); + +- return($config['system']['acb']); ++ return config_get_path('system/acb'); ++} ++ ++/* Log ACB errors when necessary. */ ++function acb_error_log($data) { ++ global $acb_base_url; ++ $notice_text = sprintf( ++ gettext("An error occurred while uploading the encrypted %s configuration backup to %s (%s)"), ++ g_get('product_label'), ++ $acb_base_url, ++ htmlspecialchars($data)); ++ log_error($notice_text . " - " . $data); ++ file_notice("AutoConfigBackup", $notice_text); ++ update_filter_reload_status($notice_text); ++} ++ ++/* Generate a self-contained HTML download link for a device key string. */ ++function acb_key_download_link($name, $key) { ++ $hostname = config_get_path('system/hostname') . "." . config_get_path('system/domain'); ++ $dltext = gettext('Download This Key'); ++ $keystring = base64_encode($key . "\n"); ++ ++ return << ++ ++EOL; ++ + } +diff -rub --new-file old/src/etc/inc/config.lib.inc new/src/etc/inc/config.lib.inc +--- old/src/etc/inc/config.lib.inc 2025-01-24 13:42:01.719398730 -0500 ++++ new/src/etc/inc/config.lib.inc 2025-01-24 13:49:11.668097998 -0500 +@@ -722,16 +722,19 @@ + run_plugins("/usr/local/pkg/write_config/"); + } + +- // Try the core AutoConfigBackup system +- if (is_array($config['system']['acb']) && $config['system']['acb']['enable'] == "yes" && +- (!isset($config['system']['acb']['frequency']) || $config['system']['acb']['frequency'] == "every") || file_exists("/tmp/forceacb")) { +- if ($doacb) { ++ /* Trigger AutoConfigBackup if necessary. */ + require_once("acb.inc"); +- upload_config($manual_acb); ++ global $acb_force_file; ++ $acb_config = config_get_path('system/acb', []); ++ if (acb_enabled() && ++ (!isset($acb_config['frequency']) || ++ ($acb_config['frequency'] == "every")) || ++ file_exists($acb_force_file)) { ++ if ($doacb) { ++ acb_backup_stage_upload($manual_acb); + } +- +- if (file_exists("/tmp/forceacb")) { +- unlink("/tmp/forceacb"); ++ if (file_exists($acb_force_file)) { ++ unlink($acb_force_file); + } + } + +diff -rub --new-file old/src/etc/inc/priv.defs.inc new/src/etc/inc/priv.defs.inc +--- old/src/etc/inc/priv.defs.inc 2025-01-24 13:50:02.375652713 -0500 ++++ new/src/etc/inc/priv.defs.inc 2025-01-24 13:50:59.153273921 -0500 +@@ -538,6 +538,12 @@ + $priv_list['page-services-acb-backup']['match'] = array(); + $priv_list['page-services-acb-backup']['match'][] = "services_acb_backup.php*"; + ++$priv_list['page-services-acb-changekey'] = array(); ++$priv_list['page-services-acb-changekey']['name'] = gettext("WebCfg - Services: Auto Config Backup: Change Device Key"); ++$priv_list['page-services-acb-changekey']['descr'] = gettext("Change the auto config backup device key."); ++$priv_list['page-services-acb-changekey']['match'] = array(); ++$priv_list['page-services-acb-changekey']['match'][] = "services_acb_changekey.php*"; ++ + $priv_list['page-services-acb-settings'] = array(); + $priv_list['page-services-acb-settings']['name'] = gettext("WebCfg - Services: Auto Config Backup: Settings"); + $priv_list['page-services-acb-settings']['descr'] = gettext("Configure the auto config backup system."); +diff -rub --new-file old/src/usr/local/sbin/acbupload.php new/src/usr/local/sbin/acbupload.php +--- old/src/usr/local/sbin/acbupload.php 2025-01-24 13:42:01.874400422 -0500 ++++ new/src/usr/local/sbin/acbupload.php 2025-01-24 13:52:47.719461962 -0500 +@@ -1,11 +1,11 @@ + (60 * 60)) { +- // The lock file is more than an hour old. Something probably went wrong ++ if (time() - filemtime($lockfile) > (60 * 60)) { ++ /* The lock file is more than an hour old. ++ * Something probably went wrong. */ + unlink_if_exists($lockfile); +- log_error("Stale ACB lock file removed"); ++ log_error(gettext('Stale AutoConfigBackup lock file removed')); + } else { + exit(); + } +@@ -43,113 +55,26 @@ + + touch($lockfile); + +-// Location of backup file pairs +-$acbuploadpath = g_get('acbbackuppath'); +-// systems we do not allow +-$badreasons = array("snort", "pfblocker", "minicron", "merged in config"); +-// List any ACB file pairs that have been created +-$files = glob($acbuploadpath . '*.form'); ++/* List file pairs ACB has staged for upload */ ++$files = glob(g_get('acbbackuppath') . '*.form'); + + if (count($files) > 0) { +- // Sort them, oldest first ++ /* Sort staged backups, oldest first */ + usort($files, function($a, $b) { + return filemtime($a) - filemtime($b); + }); + +- // Upload them to the server ++ /* Upload staged backups to the ACB service */ ++ $notice_text = sprintf(gettext('Starting upload of staged AutoConfigBackup encrypted configuration backups to %s'), $acb_base_url); ++ log_error($notice_text); ++ update_filter_reload_status($notice_text); + foreach ($files as $file) { + $basename = basename($file, ".form"); +- upload($basename); ++ acb_backup_upload($basename); ++ /* Sleep for one second between uploads to help ensure the ++ * ACB service gives each backup entry a unique timestamp. */ ++ sleep(1); + } + } + + unlink_if_exists($lockfile); +- +-function upload($basename) { +- global $acbuploadpath, $badreasons; +- +- $upload_url = "https://acb.netgate.com/save"; +- +- if (!is_url_hostname_resolvable($upload_url)) { +- $data = " Unable to resolve " . parse_url($upload_url, PHP_URL_HOST) . " "; +- acb_error_log($upload_url, $data); +- unlink_if_exists($acbuploadpath . $basename . ".data"); +- unlink_if_exists($acbuploadpath . $basename . ".form"); +- return; +- } +- +- // Retrieve the data to send +- // Retrieve the form data +- $formdata = file_get_contents($acbuploadpath . $basename . ".form"); +- $post_fields = json_decode($formdata, true); +- // Add the backup data file +- $post_fields['file'] = curl_file_create($acbuploadpath . $basename . ".data", 'image/jpg', 'config.jpg'); +- +- // Ensure there are no backups from systems we do not allow +- foreach ($badreasons as $term) { +- if (strpos(strtolower($post_fields['reason']), $term) !== false) { +- log_error("Skipping ACB backup for " . strtolower($post_fields['reason']) . '.'); +- unlink_if_exists($acbuploadpath . $basename . ".data"); +- unlink_if_exists($acbuploadpath . $basename . ".form"); +- return; +- } +- } +- +- // Check configuration into the ESF repo (Copied from /etc/inc/acb.inc) +- $curl_session = curl_init(); +- +- curl_setopt($curl_session, CURLOPT_URL, $upload_url); +- curl_setopt($curl_session, CURLOPT_POST, count($post_fields)); +- curl_setopt($curl_session, CURLOPT_POSTFIELDS, $post_fields); +- curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, 1); +- curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($curl_session, CURLOPT_CONNECTTIMEOUT, 55); +- curl_setopt($curl_session, CURLOPT_TIMEOUT, 30); +- curl_setopt($curl_session, CURLOPT_USERAGENT, g_get('product_label') . '/' . rtrim(file_get_contents("/etc/version"))); +- // Proxy +- set_curlproxy($curl_session); +- +- $data = curl_exec($curl_session); +- $httpcode = curl_getinfo($curl_session, CURLINFO_RESPONSE_CODE); +- +- if (curl_errno($curl_session)) { +- $fd = fopen("/tmp/backupdebug.txt", "w"); +- $acb_curl_error = curl_error($curl_session); +- fwrite($fd, $upload_url . "" . $fields_string . "\n\n"); +- fwrite($fd, $data); +- fwrite($fd, $acb_curl_error); +- fclose($fd); +- } else { +- curl_close($curl_session); +- } +- +- // Delete the backup files, whether it worked or not +- unlink_if_exists($acbuploadpath . $basename . ".data"); +- unlink_if_exists($acbuploadpath . $basename . ".form"); +- +- if (strpos(strval($httpcode), '20') === false) { +- if (empty($data) && $acb_curl_error) { +- $data = $acb_curl_error; +- } else { +- $data = "Unknown error"; +- } +- acb_error_log($upload_url, $data); +- } else { +- // Update last pfS backup time +- $fd = fopen("/cf/conf/lastpfSbackup.txt", "w"); +- fwrite($fd, $config['revision']['time']); +- fclose($fd); +- $notice_text = "End of configuration backup to " . $upload_url . " (success)."; +- log_error($notice_text); +- update_filter_reload_status($notice_text); +- } +-} +- +-function acb_error_log($upload_url, $data) { +- $notice_text = sprintf(gettext( +- "An error occurred while uploading the encrypted %s configuration to "), g_get('product_label')) . +- $upload_url . " (" . htmlspecialchars($data) . ")"; +- log_error($notice_text . " - " . $data); +- file_notice("AutoConfigBackup", $notice_text); +- update_filter_reload_status($notice_text); +-} +diff -rub --new-file old/src/usr/local/sbin/execacb.php new/src/usr/local/sbin/execacb.php +--- old/src/usr/local/sbin/execacb.php 2025-01-24 13:42:02.033402158 -0500 ++++ new/src/usr/local/sbin/execacb.php 2025-01-24 13:52:47.719461962 -0500 +@@ -5,7 +5,7 @@ + * part of pfSense (https://www.pfsense.org) + * Copyright (c) 2004-2013 BSD Perimeter + * Copyright (c) 2013-2016 Electric Sheep Fencing +- * Copyright (c) 2014-2023 Rubicon Communications, LLC (Netgate) ++ * Copyright (c) 2014-2025 Rubicon Communications, LLC (Netgate) + * Copyright (c) 2009 Erik Kristensen + * All rights reserved. + * +@@ -26,15 +26,18 @@ + * limitations under the License. + */ + +- // Called from cron, this script makes a remote backup of the current configuration +- // via the ACB system ++/* ++ * This script is called by cron to perform a periodic scheduled backup of the ++ * current configuration using the AutoConfigBackup service. ++ */ + +- require_once("config.inc"); +- require_once("acb.inc"); ++require_once("acb.inc"); + +- if ($config['system']['acb']['enable'] == "yes") { +- write_config("Scheduled backup"); +- upload_config(); ++if (acb_enabled()) { ++ if (is_acb_upload_needed()) { ++ write_config(gettext('Scheduled Backup via AutoConfigBackup')); ++ acb_backup_stage_upload(); ++ } else { ++ log_error(gettext('Skipping AutoConfigBackup scheduled backup (no changes since previous backup)')); + } +- +- ?> ++} +diff -rub --new-file old/src/usr/local/www/services_acb_backup.php new/src/usr/local/www/services_acb_backup.php +--- old/src/usr/local/www/services_acb_backup.php 2025-01-24 13:42:02.362405750 -0500 ++++ new/src/usr/local/www/services_acb_backup.php 2025-01-29 13:15:21.775471367 -0500 +@@ -1,11 +1,11 @@ + 900) { ++ $input_errors[] = gettext("Reason text must be less than 900 characters in length."); ++ } ++ if (is_acb_ignored_reason($_POST['reason'])) { ++ $input_errors[] = gettext("Reason text contains keywords ignored by AutoConfigBackup and will not be uploaded."); ++ } + } + +- touch("/tmp/forceacb"); + +- if ($_REQUEST['reason']) { +- if (write_config($_REQUEST['reason'] . "-MaNuAlBaCkUp")) { +- $savemsg = "Backup queued successfully."; ++ if (empty($input_errors)) { ++ global $acb_force_file, $acb_last_backup_file; ++ touch($acb_force_file); ++ if ($_POST['reason']) { ++ if (write_config($_POST['reason'] . "-MaNuAlBaCkUp")) { ++ $savemsg = gettext('Backup queued successfully.'); + } +- } elseif (write_config("Backup invoked via Auto Config Backup." . "-MaNuAlBaCkUp")) { +- $savemsg = "Backup queued successfully."; ++ } elseif (write_config(gettext('Backup invoked via Auto Config Backup.') . '-MaNuAlBaCkUp')) { ++ $savemsg = gettext('Backup queued successfully.'); + } else { +- $savemsg = "Backup not completed - write_config() failed."; ++ $savemsg = gettext('Backup not completed -- write_config() failed.'); + } + ++ global $config; + $config = parse_config(true); +- unlink_if_exists("/cf/conf/lastpfSbackup.txt"); +- +- $donotshowheader = true; ++ unlink_if_exists($acb_last_backup_file); ++ } + } + + $pgtitle = array("Services", "Auto Configuration Backup", "Backup Now"); +@@ -69,33 +78,47 @@ + $tab_array = array(); + $tab_array[] = array("Settings", false, "/services_acb_settings.php"); + $tab_array[] = array("Restore", false, "/services_acb.php"); +-$tab_array[] = array("Backup now", true, "/services_acb_backup.php"); ++$tab_array[] = array("Backup Now", true, "/services_acb_backup.php"); + display_top_tabs($tab_array); + +-$form = new Form("Backup", config_get_path('system/acb/enable') === "yes"); ++$form = new Form("Backup", acb_enabled()); + +-$section = new Form_Section('Backup Details'); ++if (acb_enabled()) { ++ $section = new Form_Section('Backup Details'); + +-$section->addInput(new Form_Input( ++ $section->addInput(new Form_Input( + 'reason', + 'Revision Reason', + 'text', +- $_REQUEST['reason'] +-))->setWidth(7)->setHelp("Enter the reason for the backup"); ++ $_POST['reason'] ++ ))->setWidth(7)->setHelp('Enter the reason for the backup. ' . ++ 'Must be 900 characters in length or less.'); ++ ++ $form->add($section); + +-$form->add($section); ++ $section = new Form_Section('Device Key'); + +-$section2 = new Form_Section('Device key'); ++ $userkey = get_acb_device_key(); + +-$section2->addInput(new Form_Input( ++ $section->addInput(new Form_Input( + 'devkey', +- 'Device key', ++ 'Device Key', + 'text', + $userkey +-))->setWidth(7)->setReadonly()->setHelp("ID used to identify this firewall (derived from the SSH public key.) " . +- "Keep a record of this key in case you should ever need to recover this backup on another firewall."); +- +-$form->add($section2); ++ ))->setWidth(7)->setReadonly()->setHelp('Unique key which identifies backups associated with this device.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this device will be lost!', ++ '
', '', '', acb_key_download_link('device', $userkey)); ++ ++ $form->add($section); ++} else { ++ $section = new Form_Section('AutoConfigBackup Disabled'); ++ $section->addInput(new Form_StaticText( ++ null, ++ 'The AutoConfigBackup service is currently disabled, manual backups are not possible.' ++ ))->setHelp('Enable AutoConfigBackup on the %sSettings tab%s.', '', ''); ++ $form->add($section); ++} + + print($form); + ?> +@@ -105,8 +128,8 @@ + events.push(function() { + $(form).submit(function(e) { + e.preventDefault(); +- encpwd = ''; +- if ( encpwd.length === 0) { ++ encpwdl = ''; ++ if ( encpwdl === 0) { + alert(''); + } else if ($('#devkey').val().length === 0 ) { + alert(''); +@@ -121,4 +144,3 @@ + + + +- +diff -rub --new-file old/src/usr/local/www/services_acb_changekey.php new/src/usr/local/www/services_acb_changekey.php +--- old/src/usr/local/www/services_acb_changekey.php 1969-12-31 19:00:00.000000000 -0500 ++++ new/src/usr/local/www/services_acb_changekey.php 2025-01-24 14:09:37.599580070 -0500 +@@ -0,0 +1,193 @@ ++. ++ * All rights reserved. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++##|+PRIV ++##|*IDENT=page-services-acb-changekey ++##|*NAME=Services: Auto Config Backup: Change Device Key ++##|*DESCR=Change the auto config backup device key. ++##|*MATCH=services_acb_changekey.php* ++##|-PRIV ++ ++require_once("guiconfig.inc"); ++require_once("functions.inc"); ++require_once("pfsense-utils.inc"); ++require_once("services.inc"); ++require_once("acb.inc"); ++ ++$pconfig = config_get_path('system/acb', []); ++ ++if ($_POST['generatekey']) { ++ print json_encode(['newdevicekey' => acb_generate_device_key()]); ++ exit; ++} ++ ++$userkey = get_acb_device_key(); ++ ++if (isset($_POST['updatekey'])) { ++ unset($input_errors); ++ ++ /* Add validation */ ++ if (!is_valid_acb_device_key($_POST['devkey'])) { ++ $input_errors[] = gettext("Invalid Device Key value."); ++ } ++ ++ if (!$input_errors) { ++ /* Update key */ ++ config_set_path('system/acb/device_key', $_POST['devkey']); ++ write_config(sprintf(gettext('Changed the AutoConfigBackup device key from %s to %s'), ++ $userkey, ++ $_POST['devkey'])); ++ Header("Location: /services_acb_settings.php"); ++ exit; ++ } ++} ++ ++$pgtitle = array(gettext("Services"), gettext("Auto Configuration Backup"), gettext("Change Device Key")); ++include("head.inc"); ++ ++if ($input_errors) { ++ print_input_errors($input_errors); ++} ++ ++$tab_array = array(); ++$tab_array[] = array("Settings", false, "/services_acb_settings.php"); ++$tab_array[] = array("Restore", false, "/services_acb.php"); ++$tab_array[] = array("Backup Now", false, "/services_acb_backup.php"); ++$tab_array[] = array("Change Device Key", true, "/services_acb_changekey.php"); ++display_top_tabs($tab_array); ++ ++$savebutton = new Form_Button( ++ 'updatekey', ++ 'Update Key', ++ null, ++ 'fa fa-save icon-embed-btn' ++); ++$savebutton->addClass('btn-danger')->setAttribute('disabled', true); ++ ++$form = new Form($savebutton); ++$section = new Form_Section('Change Device Key'); ++ ++$legacy_key = get_acb_legacy_device_key(); ++ ++if (!is_valid_acb_device_key($legacy_key) || ++ ($legacy_key != $userkey)) { ++ $device_key_backups = acb_backup_list($userkey); ++ $section->addInput(new Form_Input( ++ 'currentkey', ++ 'Current Device Key', ++ 'text', ++ $userkey ++ ))->setWidth(7)->setReadonly()->setHelp('Unique key which identifies backups associated with this device.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this device key will be lost!%1$s%1$s' . ++ 'Hosted backups for this device key: %5$d', ++ '
', '', '', acb_key_download_link('device', $userkey), count($device_key_backups)); ++} ++ ++if (is_valid_acb_device_key($legacy_key)) { ++ $legacy_key_backups = acb_backup_list($legacy_key); ++ $section->addInput(new Form_Input( ++ 'legacykey', ++ 'Legacy Device Key', ++ 'text', ++ $legacy_key ++ ))->setWidth(7)->setReadonly()->setHelp('Unique key which identifies backups associated with this device.%1$s%1$s' . ++ 'This is a legacy style key derived from the SSH public key.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this legacy device key will be lost!%1$s%1$s' . ++ 'Hosted backups for this legacy key: %5$d', ++ '
', '', '', acb_key_download_link('legacy', $userkey), count($legacy_key_backups)); ++} ++ ++$group = new Form_Group("New Device Key"); ++ ++$group->add(new Form_Input( ++ 'devkey', ++ 'Device Key', ++ 'text', ++ "" ++))->setWidth(7)->setHelp('New device key, replaces the Current Device Key.%1$s%1$s' . ++ 'Use the %2$sGenerate New Key%3$s button to create a new randomized key in the proper format, or ' . ++ 'paste a properly formatted key into the field. Keys must be 64 character hexadecimal strings (0-9, a-f).%1$s%1$s' . ++ '%2$sTreat this key as a secret!%3$s%1$sAnyone who has this key can manipulate the backups for this key.%1$s%1$s' . ++ '%2$sChanging the Device Key removes the existing device key!%3$s%1$sUse the download icon above to save a copy ' . ++ 'of the old key before continuing!', ++ '
', '', ''); ++ ++$group->add(new Form_Button( ++ 'generatekey', ++ 'Generate New Key', ++ null, ++ 'fa fa-refresh' ++))->addClass('btn-info btn-xs'); ++ ++$section->add($group); ++ ++$section->addInput(new Form_Checkbox( ++ 'confirmation', ++ 'Warning', ++ 'Check this box to acknowledge that the Device Key will change, disconnecting from previous backups', ++ false ++))->setHelp('Checking this box enables the Update Key button. ' . ++ 'Save a copy of the old Device Key before continuing.%1$s%1$s' . ++ 'Old backups are not automatically removed from the server and must be removed manually.', ++ '
'); ++ ++$form->add($section); ++ ++print $form; ++ ++?> ++
++ ++ ++ ++ +diff -rub --new-file old/src/usr/local/www/services_acb.php new/src/usr/local/www/services_acb.php +--- old/src/usr/local/www/services_acb.php 2025-01-24 13:42:02.198403960 -0500 ++++ new/src/usr/local/www/services_acb.php 2025-01-24 14:36:00.102033387 -0500 +@@ -5,7 +5,7 @@ + * part of pfSense (https://www.pfsense.org) + * Copyright (c) 2008-2013 BSD Perimeter + * Copyright (c) 2013-2016 Electric Sheep Fencing +- * Copyright (c) 2014-2023 Rubicon Communications, LLC (Netgate) ++ * Copyright (c) 2014-2025 Rubicon Communications, LLC (Netgate) + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); +@@ -28,141 +28,84 @@ + ##|*MATCH=services_acb.php* + ##|-PRIV + +-require("guiconfig.inc"); +-require("acb.inc"); ++require_once("guiconfig.inc"); ++require_once("pfsense-utils.inc"); ++require_once("acb.inc"); + +-// Separator used during client / server communications +-$oper_sep = "\|\|"; +-$exp_sep = '||'; +- +-// Encryption password + $decrypt_password = config_get_path('system/acb/encryption_password'); + +-// Defined username. Username must be sent lowercase. See Redmine #7127 and Netgate Redmine #163 +-$username = strtolower(config_get_path('system/acb/gold_username')); +-$password = config_get_path('system/acb/gold_password'); +- +-// Set hostname +-if ($_REQUEST['hostname']) { +- $hostname = $_REQUEST['hostname']; +-} else { +- $hostname = config_get_path('system/hostname') . "." . config_get_path('system/domain'); +-} +- +-// Hostname of local machine +-$myhostname = config_get_path('system/hostname') . "." . config_get_path('system/domain'); +- ++/* Check if the ACB configuration contains an encryption password. ++ * If it does not, then redirect user to the settings page. ++ */ + if (!$decrypt_password) { + Header("Location: /services_acb_settings.php"); + exit; + } + +-if ($_REQUEST['savemsg']) { +- $savemsg = htmlentities($_REQUEST['savemsg']); ++$input_errors = []; ++ ++$origkey = get_acb_device_key(); ++$userkey = $origkey; ++ ++if (isset($_POST['userkey'])) { ++ if (is_valid_acb_device_key($_POST['userkey'])) { ++ $userkey = htmlentities($_POST['userkey']); ++ } else { ++ $input_errors[] = gettext("Invalid Device Key value"); ++ unset($_POST['userkey']); ++ } ++} ++ ++if ($_POST['savemsg']) { ++ $savemsg = htmlentities($_POST['savemsg']); + } + +-if ($_REQUEST['download']) { ++if ($_POST['view'] && ++ is_valid_acb_revision($_POST['view'])) { + $pgtitle = array("Services", "Auto Configuration Backup", "Revision Information"); + } else { + $pgtitle = array("Services", "Auto Configuration Backup", "Restore"); ++ unset($_POST['view']); + } + +-/* Set up time zones for conversion. See #5250 */ +-$acbtz = new DateTimeZone('America/Chicago'); +-$mytz = new DateTimeZone(date_default_timezone_get()); +- + include("head.inc"); + ++if (($_POST['rmver'] != "") && ++ is_valid_acb_revision($_POST['rmver'])) { ++ $savemsg = acb_backup_delete($userkey, $_POST['rmver']); ++} else { ++ unset($_POST['rmver']); ++} + +-if ($_REQUEST['rmver'] != "") { +- $curl_session = curl_init(); +- curl_setopt($curl_session, CURLOPT_URL, "https://acb.netgate.com/rmbkp"); +- curl_setopt($curl_session, CURLOPT_POSTFIELDS, "userkey=" . $userkey . +- "&revision=" . urlencode($_REQUEST['rmver']) . +- "&version=" . g_get('product_version') . +- "&uid=" . urlencode($uniqueID)); +- curl_setopt($curl_session, CURLOPT_POST, 3); +- curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, 1); +- curl_setopt($curl_session, CURLOPT_USERAGENT, g_get('product_label') . '/' . rtrim(file_get_contents("/etc/version"))); +- // Proxy +- set_curlproxy($curl_session); +- +- $data = curl_exec($curl_session); +- if (curl_errno($curl_session)) { +- $fd = fopen("/tmp/acb_deletedebug.txt", "w"); +- fwrite($fd, "https://acb.netgate.com/rmbkp" . "" . "action=delete&hostname=" . urlencode($hostname) . "&revision=" . urlencode($_REQUEST['rmver']) . "\n\n"); +- fwrite($fd, $data); +- fwrite($fd, curl_error($curl_session)); +- fclose($fd); +- $savemsg = "An error occurred while trying to remove the item from acb.netgate.com."; ++if (($_POST['newver'] != "") && ++ is_valid_acb_revision($_POST['newver'])) { ++ [$encrypted_backup, $httpcode, $errno] = acb_backup_get($userkey, $_POST['newver']); ++ if ($errno) { ++ $input_errors[] = sprintf(gettext('Unable to get backup revision from AutoConfigBackup service: %s'), htmlspecialchars($_POST['newver'])); + } else { +- curl_close($curl_session); +- $budate = new DateTime($_REQUEST['rmver'], $acbtz); +- $budate->setTimezone($mytz); +- $savemsg = "Backup revision " . htmlspecialchars($budate->format(DATE_RFC2822)) . " has been removed."; ++ [$decrypted_config, $encrypted_config, $sha256, $decrypt_errors] = acb_backup_decrypt($encrypted_backup, $decrypt_password); + } +-} + +-if ($_REQUEST['newver'] != "") { +- // Phone home and obtain backups +- $curl_session = curl_init(); +- +- curl_setopt($curl_session, CURLOPT_URL, "https://acb.netgate.com/getbkp"); +- curl_setopt($curl_session, CURLOPT_POSTFIELDS, "userkey=" . $userkey . +- "&revision=" . urlencode($_REQUEST['newver']) . +- "&version=" . g_get('product_version') . +- "&uid=" . urlencode($uniqueID)); +- curl_setopt($curl_session, CURLOPT_POST, 3); +- curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, 1); +- curl_setopt($curl_session, CURLOPT_USERAGENT, g_get('product_label') . '/' . rtrim(file_get_contents("/etc/version"))); +- // Proxy +- set_curlproxy($curl_session); +- $data = curl_exec($curl_session); +- $data_split = explode('++++', $data); +- $sha256 = trim($data_split[0]); +- $data = $data_split[1]; +- +- if (!tagfile_deformat($data, $data, "config.xml")) { +- $input_errors[] = "The downloaded file does not appear to contain an encrypted pfSense configuration."; +- } +- +- $out = decrypt_data($data, $decrypt_password); +- if (!strstr($out, "pfsense") || +- (strlen($out) < 50)) { +- $out = "Could not decrypt. Different encryption key?"; +- $input_errors[] = "Could not decrypt config.xml. Check the encryption key and try again: {$out}"; ++ if (!empty($decrypt_errors)) { ++ $input_errors = array_merge($input_errors, $decrypt_errors); + } else { +- $pos = stripos($out, ""); +- $data = substr($out, 0, $pos); +- $data = $data . "\n"; +- +- $fd = fopen("/tmp/config_restore.xml", "w"); +- fwrite($fd, $data); +- fclose($fd); +- +- $ondisksha256 = trim(shell_exec("/sbin/sha256 /tmp/config_restore.xml | /usr/bin/awk '{ print $4 }'")); +- // We might not have a sha256 on file for older backups +- if ($sha256 != "0" && $sha256 != "") { ++ $config_restore_path = '/tmp/config_restore.xml'; ++ file_put_contents($config_restore_path, $decrypted_config); ++ ++ $ondisksha256 = hash_file('sha256', $config_restore_path); ++ /* ACB may not have a sha256 hash on file for older backup entries */ ++ if (($sha256 != "0") && ++ ($sha256 != "")) { + if ($ondisksha256 != $sha256) { +- $input_errors[] = "SHA256 values do not match, cannot restore. $ondisksha256 != $sha256"; ++ $input_errors[] = "SHA256 values do not match, cannot restore. ({$ondisksha256} != {$sha256})"; + } + } +- if (curl_errno($curl_session)) { +- /* If an error occurred, log the error in /tmp/ */ +- $fd = fopen("/tmp/acb_restoredebug.txt", "w"); +- fwrite($fd, "https://acb.netgate.com/getbkp" . "" . "action=restore&hostname={$hostname}&revision=" . urlencode($_REQUEST['newver']) . "\n\n"); +- fwrite($fd, $data); +- fwrite($fd, curl_error($curl_session)); +- fclose($fd); +- } else { +- curl_close($curl_session); +- } + +- if (!$input_errors && $data) { ++ if (!$input_errors && $decrypted_config) { + if (config_restore("/tmp/config_restore.xml") == 0) { +- $savemsg = "Successfully reverted the pfSense configuration to revision " . urldecode($_REQUEST['newver']) . "."; ++ $savemsg = sprintf(gettext('Successfully reverted the %s configuration to revision %s.'), ++ g_get('product_label'), ++ $_POST['newver']); + $savemsg .= << + +@@ -172,112 +115,50 @@ + + EOF; + } else { +- $savemsg = "Unable to revert to the selected configuration."; ++ $errormsg = gettext('Unable to revert to the selected configuration backup revision.'); + } + } else { +- log_error("There was an error when restoring the AutoConfigBackup item"); ++ log_error(gettext('There was an error while restoring the AutoConfigBackup entry')); + } +- unlink_if_exists("/tmp/config_restore.xml"); ++ unlink_if_exists($config_restore_path); + } ++} else { ++ unset($_POST['newver']); + } + +-if ($_REQUEST['download']) { +- // Phone home and obtain backups +- $curl_session = curl_init(); +- +- curl_setopt($curl_session, CURLOPT_URL, "https://acb.netgate.com/getbkp"); +- curl_setopt($curl_session, CURLOPT_POSTFIELDS, "userkey=" . $userkey . "&revision=" . urlencode($_REQUEST['download'])); +- curl_setopt($curl_session, CURLOPT_POST, 3); +- curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, 1); +- +- curl_setopt($curl_session, CURLOPT_USERAGENT, g_get('product_label') . '/' . rtrim(file_get_contents("/etc/version"))); +- // Proxy +- set_curlproxy($curl_session); +- $data = curl_exec($curl_session); +- +- if (curl_errno($curl_session)) { +- $fd = fopen("/tmp/acb_backupdebug.txt", "w"); +- fwrite($fd, "https://acb.netgate.com/getbkp" . "" . "action=sgetbackup" . "\n\n"); +- fwrite($fd, $data); +- fwrite($fd, curl_error($curl_session)); +- fclose($fd); ++if ($_POST['view'] && ++ is_valid_acb_revision($_POST['view'])) { ++ [$encrypted_backup, $httpcode, $errno] = acb_backup_get($userkey, $_POST['view']); ++ if ($errno) { ++ $input_errors[] = sprintf(gettext('Unable to get backup revision from AutoConfigBackup service: %s'), htmlspecialchars($_POST['view'])); + } else { +- curl_close($curl_session); ++ [$decrypted_config, $encrypted_config, $sha256sum, $decrypt_errors] = acb_backup_decrypt($encrypted_backup, $decrypt_password); + } + +- if (!tagfile_deformat($data, $data1, "config.xml")) { +- $input_errors[] = "The downloaded file does not appear to contain an encrypted pfSense configuration."; ++ if (!empty($decrypt_errors)) { ++ $input_errors = array_merge($input_errors, $decrypt_errors); + } else { +- $ds = explode('++++', $data); +- $revision = $_REQUEST['download']; +- $sha256sum = $ds[0]; ++ $revision = $_POST['view']; + if ($sha256sum == "0") { + $sha256sum = "None on file."; + } +- $data = $ds[1]; + $configtype = "Encrypted"; +- if (!tagfile_deformat($data, $data, "config.xml")) { +- $input_errors[] = "The downloaded file does not appear to contain an encrypted pfSense configuration."; +- } +- $data = decrypt_data($data, $decrypt_password); +- if (!strstr($data, "pfsense")) { +- $data = "Could not decrypt. Different encryption key?"; +- $input_errors[] = "Could not decrypt config.xml. Check the encryption key and try again."; + } ++ if ($_POST['download'] == 'true') { ++ $hostname = config_get_path('system/hostname') . "." . config_get_path('system/domain'); ++ $revision = acb_time_shift($_POST['view'], "YmdHis"); ++ send_user_download('data', $decrypted_config, "config-backup-acb-{$hostname}-{$revision}.xml", "text/xml"); + } ++} else { ++ unset($_POST['view']); + } + +-// $confvers must be populated viewing info but there were errors +-$confvers = array(); +-if ((!($_REQUEST['download']) || $input_errors) && check_dnsavailable()) { +- // Populate available backups +- $curl_session = curl_init(); +- +- curl_setopt($curl_session, CURLOPT_URL, "https://acb.netgate.com/list"); +- curl_setopt($curl_session, CURLOPT_POSTFIELDS, "userkey=" . $userkey . +- "&uid=eb6a4e6f76c10734b636" . +- "&version=" . g_get('product_version') . +- "&uid=" . urlencode($uniqueID)); +- curl_setopt($curl_session, CURLOPT_SSL_VERIFYPEER, 1); +- curl_setopt($curl_session, CURLOPT_POST, 1); +- curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, 1); +- +- curl_setopt($curl_session, CURLOPT_USERAGENT, g_get('product_label') . '/' . rtrim(file_get_contents("/etc/version"))); +- // Proxy +- set_curlproxy($curl_session); +- +- $data = curl_exec($curl_session); +- +- if (curl_errno($curl_session)) { +- $fd = fopen("/tmp/acb_backupdebug.txt", "w"); +- fwrite($fd, "https://acb.netgate.com/list" . "" . "action=showbackups" . "\n\n"); +- fwrite($fd, $data); +- fwrite($fd, curl_error($curl_session)); +- fclose($fd); +- } else { +- curl_close($curl_session); +- } +- +- // Loop through and create new confvers +- $data_split = explode("\n", $data); +- +- foreach ($data_split as $ds) { +- $ds_split = explode($exp_sep, $ds); +- $tmp_array = array(); +- $tmp_array['username'] = $ds_split[0]; +- $tmp_array['reason'] = $ds_split[1]; +- $tmp_array['time'] = $ds_split[2]; +- +- /* Convert the time from server time to local. See #5250 */ +- $budate = new DateTime($tmp_array['time'], $acbtz); +- $budate->setTimezone($mytz); +- $tmp_array['localtime'] = $budate->format(DATE_RFC2822); ++/* $confvers must be an array. */ ++$confvers = []; + +- if ($ds_split[2] && $ds_split[0]) { +- $confvers[] = $tmp_array; +- } +- } ++if ((!($_POST['view']) || $input_errors) && ++ acb_check_dns()) { ++ $confvers = acb_backup_list($userkey); + } + + if ($input_errors) { +@@ -286,54 +167,61 @@ + if ($savemsg) { + print_info_box($savemsg, 'success'); + } ++if ($errormsg) { ++ print_info_box($errormsg, 'danger'); ++} + + $tab_array = array(); + $tab_array[0] = array("Settings", false, "/services_acb_settings.php"); +-if ($_REQUEST['download']) { +- $active = false; +-} else { +- $active = true; ++$tab_array[1] = array("Restore", !($_POST['view']), "/services_acb.php"); ++if ($_POST['view']) { ++ $tab_array[] = array("Revision", true, "/services_acb.php?view=" . htmlspecialchars($_POST['view'])); + } +- +-$tab_array[1] = array("Restore", $active, "/services_acb.php"); +- +-if ($_REQUEST['download']) { +- $tab_array[] = array("Revision", true, "/services_acb.php?download=" . htmlspecialchars($_REQUEST['download'])); +-} +- +-$tab_array[] = array("Backup now", false, "/services_acb_backup.php"); ++$tab_array[] = array("Backup Now", false, "/services_acb_backup.php"); + + display_top_tabs($tab_array); + ?> + +
+- Loading, please wait... ++ Loading, please wait... +
+ + +-addInput(new Form_Input( +- 'download', +- 'Revision date/time', ++ 'view', ++ 'Service Date/Time', + 'text', +- $_REQUEST['download'] +-))->setWidth(7)->setReadOnly(); ++ $_POST['view'] ++))->setWidth(7)->setReadOnly()->setHelp('Date and time of this revision on the AutoConfigBackup service (UTC).'); ++ ++$local_revision_time = acb_time_shift($_POST['view'], $format = DATE_RFC2822); ++ ++$section->addInput(new Form_Input( ++ 'view', ++ 'Local Date/Time', ++ 'text', ++ $local_revision_time ++))->setWidth(7)->setReadOnly()->setHelp('Local date and time of this revision.'); ++ ++$metadata = acb_backup_get_metadata($userkey, $_POST['view']); ++$reason = (array_key_exists('reason', $metadata)) ? $metadata['reason'] : gettext("No Revision Description"); + + $section->addInput(new Form_Input( + 'reason', + 'Revision Reason', + 'text', +- $_REQUEST['reason'] ++ $reason + ))->setWidth(7)->setReadOnly(); + + $section->addInput(new Form_Input( + 'shasum', +- 'SHA256 summary', ++ 'SHA256 Summary', + 'text', + $sha256sum + ))->setWidth(7)->setReadOnly(); +@@ -341,13 +229,13 @@ + $section->addInput(new Form_Textarea( + 'config_xml', + 'Encrypted config.xml', +- $ds[1] ++ $encrypted_config + ))->setWidth(7)->setAttribute("rows", "40")->setAttribute("wrap", "off"); + + $section->addInput(new Form_Textarea( + 'dec_config_xml', + 'Decrypted config.xml', +- $data ++ $decrypted_config + ))->setWidth(7)->setAttribute("rows", "40")->setAttribute("wrap", "off"); + + $form->add($section); +@@ -355,48 +243,86 @@ + print($form); + + ?> +-')"> Install this revision ++ Restore this revision ++ ++ Download this revision + + ", $legacy_key); ++ $check_button_key = $legacy_key; ++} elseif ($userkey != $origkey) { ++ $legacy_string = sprintf(gettext('%1$s%1$sBackups may also exist under the configured key for this device: %2$s'), "
", $origkey); ++ $check_button_key = $origkey; ++} ++ ++if ($userkey == $legacy_key) { ++ $key_type = "legacy"; ++} elseif($userkey != $origkey) { ++ $key_type = "alternate"; ++} else { ++ $key_type = "device"; ++} ++ ++if (!empty($legacy_string)) { ++ /* Add a check button */ ++ $legacy_string .= ' ' . ++ gettext('Check') . '

'; ++} + $group->add(new Form_Input( + 'devkey', +- 'Device key', ++ 'Device Key', + 'text', + $userkey +-))->setWidth(7)->setHelp("ID used to identify this firewall (derived from the SSH public key.) " . +- "See help below for more details. %sPlease make a safe copy of this ID value.%s If it is lost, your backups will" . +- " be lost too!", "", ""); ++))->setWidth(7)->setHelp('Unique key which identifies backups associated with this device. ' . ++ 'See help below for more details.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this device will be lost!%5$s', ++ '
', '', '', acb_key_download_link($key_type, $userkey), $legacy_string); + + $group->add(new Form_Button( + 'upduserkey', +- 'Submit', ++ 'Search', + null, +- 'fa-save' ++ 'fa fa-search' + ))->addClass('btn-success btn-xs'); + + $group->add(new Form_Button( + 'restore', + 'Reset', + null, +- 'fa-refresh' ++ 'fa fa-undo' + ))->addClass('btn-info btn-xs'); + + $section2->add($group); + print($section2); + + print('
'); +-print_info_box(gettext("The Device key listed above is derived from the SSH public key of the firewall. When a configuration is saved, it is identified by this value." . +- " If you are restoring the configuration of another firewall, paste the Device key from that firewall into the Device ID field above and click \"Submit\"." . +- " This will temporarily override the ID for this session."), 'info', false); ++print_info_box(gettext('AutoConfigBackup uses the Device Key to associate ' . ++ 'uploaded configuration backups with a specific installation. ' . ++ 'To view or restore configuration backups from a different installation, ' . ++ 'paste its device key into the Device Key field above and click "Search". '), 'info', false); + print('
'); + + ?> +
+-

++

+
+
+
+@@ -404,9 +330,9 @@ + + + +- +- +- ++ ++ ++ + + + +@@ -416,20 +342,34 @@ + if (config_get_path('system/acb/reverse') == "yes"){ + $confvers = array_reverse($confvers); + } ++ $staged_backup_count = count(glob(g_get('acbbackuppath') . '*.form')); + + foreach ($confvers as $cv): + ?> + +- +- ++ + ++ + + +
"> ++ ++ +- ')"> +- +- +- +- ++ ++ ++ ++ ++ ++ +
+ +- + +- +- ++ + +- +- + +- +
+- ++ ++ ++ ++ ++ + +-
+-
: ++ ++ ++ 0): ?> ++
++ ++ +
+
+
+@@ -467,15 +410,14 @@ + events.push(function(){ + $('#loading').hide(); + +- // On clicking Submit", reload the page but with a POST parameter "userkey" set +- $('#upduserkey').click(function() { ++ // Submit a form to change the device key ++ function changedevkey() { + var $form = $('
'); + var newuserkey = $('#devkey').val(); +- + $form + .attr("method", "POST") + .attr("action", '/services_acb.php') +- // The CSRF magic is required because we will be viewing the results of the POST ++ // CSRF magic value is required to submit form content securely. + .append( + $("") + .attr("type", "hidden") +@@ -490,10 +432,15 @@ + ) + .appendTo('body') + .submit(); ++ } ++ ++ $('#upduserkey').click(function() { ++ changedevkey(); + }); + + $('#restore').click(function() { + $('#devkey').val(""); ++ changedevkey(); + }); + }); + //]]> +diff -rub --new-file old/src/usr/local/www/services_acb_settings.php new/src/usr/local/www/services_acb_settings.php +--- old/src/usr/local/www/services_acb_settings.php 2025-01-24 13:42:02.673409145 -0500 ++++ new/src/usr/local/www/services_acb_settings.php 2025-01-24 14:12:52.817744872 -0500 +@@ -1,11 +1,11 @@ + (int)"50" ) { +- $input_errors[] = gettext("You may not retain more than 50 manual backups."); ++ /* Check Hint/Identifier */ ++ if (!empty($_POST['hint']) && ++ (strlen($_POST['hint']) > 255)) { ++ $input_errors[] = gettext("Hint/Identifier must be less than 255 characters in length."); + } + +- $pwd = ""; ++ /* Check Manual backup limit */ ++ if (!empty($_POST['numman']) && ++ !is_numericint($_POST['numman'])) { ++ $input_errors[] = gettext("Manual Backup Limit must be blank or an integer."); ++ } ++ if ((int)$_POST['numman'] < 0) { ++ $input_errors[] = gettext("Manual Backup Limit cannot be negative."); ++ } ++ if ((int)$_POST['numman'] > 50) { ++ $input_errors[] = gettext("Manual Backup Limit cannot be larger than 50."); ++ } + +- if (!$input_errors) { +- if($update_ep) { +- $pwd = $pconfig['encryption_password']; ++ /* Check Descending Date Order */ ++ if (!empty($_POST['reverse']) && ++ ($_POST['reverse'] != 'yes')) { ++ $input_errors[] = gettext("Invalid Descending Date Order value."); + } + ++ /* Store updated ACB configuration */ ++ if (!$input_errors) { + $pconfig = setup_ACB( + $pconfig['enable'], + $pconfig['hint'], +@@ -97,7 +116,7 @@ + $pconfig['dow'], + $pconfig['numman'], + $pconfig['reverse'], +- $pwd ++ ($update_ep ? $pconfig['encryption_password'] : "") + ); + } + } +@@ -112,7 +131,7 @@ + $tab_array = array(); + $tab_array[] = array("Settings", true, "/services_acb_settings.php"); + $tab_array[] = array("Restore", false, "/services_acb.php"); +-$tab_array[] = array("Backup now", false, "/services_acb_backup.php"); ++$tab_array[] = array("Backup Now", false, "/services_acb_backup.php"); + display_top_tabs($tab_array); + + $form = new Form; +@@ -185,37 +204,75 @@ + ))->setHelp("Day of week (0-6)"); + + $group->addClass("cronsched"); +-$group->setHelp(sprintf('Use * ("every"), divisor or exact value. Minutes are randomly chosen by default. See %s for more information.', ++$group->setHelp(sprintf('Use * ("every"), divisor, or exact value. Minutes are randomly chosen by default. See %s for more information.', + 'Cron format')); + $section->add($group); + ++$group = new Form_Group("Device Key"); ++ ++$userkey = get_acb_device_key(); ++$legacy_key = get_acb_legacy_device_key(); ++$legacy_string = ""; ++if (is_valid_acb_device_key($legacy_key) && ++ ($legacy_key == $userkey)) { ++ $legacy_string = sprintf(gettext('%1$s%1$sThis is a legacy style key derived from the SSH public key.%1$s' . ++ 'The best practice is to change this key to a randomized key using the ' . ++ '%2$sChange Key%3$s button.'), ++ '
', '', ''); ++} ++ ++$group->add(new Form_Input( ++ 'devkey', ++ 'Device Key', ++ 'text', ++ $userkey ++))->setWidth(7)->setReadonly()->setHelp('Unique key which identifies backups associated with this device.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s %4$s%1$s' . ++ 'If this key is lost, all backups for this device will be lost!%5$s', ++ '
', '', '', acb_key_download_link('device', $userkey), $legacy_string); ++ ++$group->add(new Form_Button( ++ 'changekey', ++ 'Change Key', ++ null, ++ 'fa fa-key' ++))->addClass('btn-info btn-xs'); ++ ++$section->add($group); ++ + $section->addPassword(new Form_Input( + 'encryption_password', + '*Encryption Password', + 'password', + $pconfig['encryption_password'] +-))->setHelp("The best practice for security is to use a long and complex password."); ++))->setHelp('AutoConfigBackup uses this string to encrypt the contents of the backup before upload. ' . ++ 'The best practice for security is to use a long and complex string.%1$s%1$s' . ++ '%2$sKeep a secure copy of this value!%3$s%1$s' . ++ 'If this password is lost, any backups encrypted with this string will be unreadable!', ++ '
', '', ''); + + $section->addInput(new Form_Input( + 'hint', + 'Hint/Identifier', + 'text', + $pconfig['hint'] +-))->setHelp("You may optionally provide an identifier which will be stored in plain text along with each encrypted backup. " . +- "This may allow the Netgate support team to locate your key should you lose it."); ++))->setHelp("Optional identifier which AutoConfigBackup will store in plain text along with each encrypted backup. " . ++ "This may allow Netgate TAC to recover a device key should it become lost."); + + $section->addInput(new Form_Input( + 'numman', +- 'Manual backups to keep', ++ 'Manual Backup Limit', + 'number', +- $pconfig['numman'] +-))->setHelp("It may be useful to specify how many manual backups are retained on the server so that automatic backups do not overwrite them." . +- "A maximum of 50 retained manual backups (of the 100 total backups) is permitted."); ++ $pconfig['numman'], ++ ['min'=>'0', 'max'=>'50'] ++))->setHelp("Number of manual backup entries AutoConfigBackup will retain on the server, " . ++ "which will not be overwritten by automatic backups. " . ++ "The maximum value is 50 retained manual backups out of the 100 total entries."); + + $section->addInput(new Form_Checkbox( + 'reverse', +- 'Descending Order by Date', +- 'List backups in descending order', ++ 'Descending Date Order', ++ 'List backups in descending order by revision date/time', + ($pconfig['reverse'] == "yes") + ))->setHelp("List backups in descending order (newest first) when viewing the restore section."); + +@@ -228,13 +285,19 @@ + + + diff --git a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/dfc70e51d556d8c1724bfc7f1fd5fe4b73faab3b.patch b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/dfc70e51d556d8c1724bfc7f1fd5fe4b73faab3b.patch new file mode 100644 index 000000000000..3585cc8ffaf0 --- /dev/null +++ b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/dfc70e51d556d8c1724bfc7f1fd5fe4b73faab3b.patch @@ -0,0 +1,85 @@ +commit dfc70e51d556d8c1724bfc7f1fd5fe4b73faab3b +Author: jim-p +Date: Tue Apr 1 12:20:43 2025 -0400 + + Improve validation of Firewall Schedules. Fixes #16114 + +diff --git a/src/usr/local/www/firewall_schedule.php b/src/usr/local/www/firewall_schedule.php +index b05d259268..8ebf13e7a1 100644 +--- a/src/usr/local/www/firewall_schedule.php ++++ b/src/usr/local/www/firewall_schedule.php +@@ -107,8 +107,8 @@ foreach (config_get_path('schedules/schedule', []) as $schedule): + $firstDayFound = false; + $firstPrint = false; + foreach ($tempmontharray as $monthtmp) { +- $month = $tempmontharray[$arraycounter]; +- $day = $tempdayarray[$arraycounter]; ++ $month = (int)$tempmontharray[$arraycounter]; ++ $day = (int)$tempdayarray[$arraycounter]; + + if (!$firstDayFound) { + $firstDay = $day; +diff --git a/src/usr/local/www/firewall_schedule_edit.php b/src/usr/local/www/firewall_schedule_edit.php +index da66b59fa6..0ade9fd329 100644 +--- a/src/usr/local/www/firewall_schedule_edit.php ++++ b/src/usr/local/www/firewall_schedule_edit.php +@@ -76,24 +76,18 @@ if (isset($id) && $a_schedules[$id]) { + } + + if ($_POST['save']) { +- +- if (strtolower($_POST['name']) == "lan") { +- $input_errors[] = gettext("Schedule may not be named LAN."); +- } +- +- if (strtolower($_POST['name']) == "wan") { +- $input_errors[] = gettext("Schedule may not be named WAN."); +- } +- +- if (strtolower($_POST['name']) == "") { ++ if (empty($_POST['name'])) { + $input_errors[] = gettext("Schedule name cannot be blank."); + } + ++ /* Schedule names are not directly referenced in firewall rules, so they ++ * do not have to follow this format, but since this limitation was ++ * already in place, it makes for convenient validation. */ + if (!is_validaliasname($_POST['name'])) { + $input_errors[] = invalidaliasnamemsg($_POST['name'], gettext("schedule")); + } + +- /* check for name conflicts */ ++ /* Check for name conflicts */ + foreach ($a_schedules as $schedule) { + if (isset($id) && ($a_schedules[$id]) && ($a_schedules[$id] === $schedule)) { + continue; +@@ -124,6 +118,17 @@ if ($_POST['save']) { + continue; + } + ++ /* Valid schedule specifications are a comma-separated list containing ++ * or or more of: ++ * ++ * - Single digit "day of week" numbers: <1-7> ++ * - Specific days in the format: w<1-52>p<1-7>-m<1-12>d<1-31> ++ */ ++ if (!preg_match('/^([1-7]|,|w(5[0-2]|[1-4][0-9]|[0-9])p([1-7])-m(1[0-2]|[1-9])d([12][0-9]|3[01]|[1-9]))+$/', $_POST['schedule' . $x])) { ++ $input_errors[] = sprintf(gettext("Invalid schedule specification in row %d."), $x+1); ++ continue; ++ } ++ + $timerangeFound = true; + $timeparts = array(); + $firstprint = false; +@@ -489,8 +494,9 @@ if ($getSchedule && !empty($pconfig['timerange'])) { + $tempdayarray = explode(",", $timerange['day']); + $arraycounter = 0; + foreach ($tempmontharray as $monthtmp) { +- $month = $tempmontharray[$arraycounter]; +- $day = $tempdayarray[$arraycounter]; ++ $month = (int)$tempmontharray[$arraycounter]; ++ $day = (int)$tempdayarray[$arraycounter]; ++ + $daypos = date("w", mktime(0, 0, 0, date($month), date($day), date("Y"))); + //if sunday, set position to 7 to get correct week number. This is due to php limitations on ISO-8601. When we move to php5.1 we can change this. + if ($daypos == 0) { diff --git a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/manifest.inc b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/manifest.inc index 931d19c60ff7..5b4bd0647eca 100644 --- a/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/manifest.inc +++ b/sysutils/pfSense-pkg-System_Patches/files/usr/local/pkg/patches/manifest.inc @@ -25,6 +25,177 @@ global $recommended_patches; * save space. */ $recommended_patches = [ +[ + 'uniqid' => 'acb-update-24.11', + 'versions' => ['24.11'], + 'descr' => 'AutoConfigBackup Update', + 'links' => [ + [ 'text' => 'After applying this patch, consider changing the ACB device key' ], + [ 'text' => 'Redmine #12249', + 'url' => 'https://redmine.pfsense.org/issues/12249', ], + [ 'text' => 'Redmine #15927', + 'url' => 'https://redmine.pfsense.org/issues/15927', ], + [ 'text' => 'pfSense-SA-25_03.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_03.webgui.asc', ], + [ 'text' => 'CVE-2024-57273' ], + [ 'text' => 'Redmine #16010', + 'url' => 'https://redmine.pfsense.org/issues/16010', ], + [ 'text' => 'Redmine #16011', + 'url' => 'https://redmine.pfsense.org/issues/16011', ], + [ 'text' => 'Redmine #16012', + 'url' => 'https://redmine.pfsense.org/issues/16012', ], + [ 'text' => 'Redmine #16013', + 'url' => 'https://redmine.pfsense.org/issues/16013', ], + [ 'text' => 'Redmine #16014', + 'url' => 'https://redmine.pfsense.org/issues/16014', ], + [ 'text' => 'Redmine #16015', + 'url' => 'https://redmine.pfsense.org/issues/16015', ], + [ 'text' => 'Redmine #16016', + 'url' => 'https://redmine.pfsense.org/issues/16016', ], + [ 'text' => 'pfSense-SA-25_04.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_04.webgui.asc', ], + [ 'text' => 'CVE-2024-57273' ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => 'acb-update-ce-2.7.2', + 'versions' => ['2.7.2'], + 'descr' => 'AutoConfigBackup update', + 'links' => [ + [ 'text' => 'After applying this patch, consider changing the ACB device key' ], + [ 'text' => 'Redmine #12249', + 'url' => 'https://redmine.pfsense.org/issues/12249', ], + [ 'text' => 'Redmine #15927', + 'url' => 'https://redmine.pfsense.org/issues/15927', ], + [ 'text' => 'pfSense-SA-25_03.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_03.webgui.asc', ], + [ 'text' => 'CVE-2024-57273' ], + [ 'text' => 'Redmine #16010', + 'url' => 'https://redmine.pfsense.org/issues/16010', ], + [ 'text' => 'Redmine #16011', + 'url' => 'https://redmine.pfsense.org/issues/16011', ], + [ 'text' => 'Redmine #16012', + 'url' => 'https://redmine.pfsense.org/issues/16012', ], + [ 'text' => 'Redmine #16013', + 'url' => 'https://redmine.pfsense.org/issues/16013', ], + [ 'text' => 'Redmine #16014', + 'url' => 'https://redmine.pfsense.org/issues/16014', ], + [ 'text' => 'Redmine #16015', + 'url' => 'https://redmine.pfsense.org/issues/16015', ], + [ 'text' => 'Redmine #16016', + 'url' => 'https://redmine.pfsense.org/issues/16016', ], + [ 'text' => 'pfSense-SA-25_04.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_04.webgui.asc', ], + [ 'text' => 'CVE-2024-57273' ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => '15844-widgetkey-validation-24.11', + 'versions' => ['24.11'], + 'descr' => 'Fix Dashboard widget key validation', + 'links' => [ + [ 'text' => 'Redmine #15844', + 'url' => 'https://redmine.pfsense.org/issues/15844', ], + [ 'text' => 'pfSense-SA-25_01.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_01.webgui.asc', ], + [ 'text' => 'CVE-2024-54779' ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => '15844-widgetkey-validation-ce-2.7.2', + 'versions' => ['2.7.2'], + 'descr' => 'Fix Dashboard widget key validation', + 'links' => [ + [ 'text' => 'Redmine #15844', + 'url' => 'https://redmine.pfsense.org/issues/15844', ], + [ 'text' => 'pfSense-SA-25_01.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_01.webgui.asc', ], + [ 'text' => 'CVE-2024-54779' ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => '15856-openvpn-mgmt-fix', + 'versions' => ['24.11', '2.7.2'], + 'descr' => 'Fix OpenVPN status and widget validation', + 'links' => [ + [ 'text' => 'Redmine #15856', + 'url' => 'https://redmine.pfsense.org/issues/15856', ], + [ 'text' => 'pfSense-SA-25_02.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_02.webgui.asc', ], + [ 'text' => 'CVE-2024-54780' ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => 'dfc70e51d556d8c1724bfc7f1fd5fe4b73faab3b', + 'versions' => ['24.11', '2.7.2'], + 'descr' => 'Fix potential stored XSS in Firewall Schedules', + 'links' => [ + [ 'text' => 'Redmine #16114', + 'url' => 'https://redmine.pfsense.org/issues/16114', ], + [ 'text' => 'pfSense-SA-25_05.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_05.webgui.asc', ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => '0ff75cd0b9fb14c04c94c3585831a9f669be0a5d', + 'versions' => ['24.11', '2.7.2'], + 'descr' => 'Fix potential stored XSS in IPsec Phase 1', + 'links' => [ + [ 'text' => 'Redmine #16115', + 'url' => 'https://redmine.pfsense.org/issues/16115', ], + [ 'text' => 'pfSense-SA-25_06.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_06.webgui.asc', ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => '6a92af14584d22f077e1421e952674f880cd5b6c', + 'versions' => ['24.11'], + 'descr' => 'Fix potential stored XSS in Wake on LAN page and widget', + 'links' => [ + [ 'text' => 'Redmine #16116', + 'url' => 'https://redmine.pfsense.org/issues/16116', ], + [ 'text' => 'pfSense-SA-25_07.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_07.webgui.asc', ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], +[ + 'uniqid' => '6a92af14584d22f077e1421e952674f880cd5b6c-2_7_2', + 'versions' => ['2.7.2'], + 'descr' => 'Fix potential stored XSS in Wake on LAN page and widget', + 'links' => [ + [ 'text' => 'Redmine #16116', + 'url' => 'https://redmine.pfsense.org/issues/16116', ], + [ 'text' => 'pfSense-SA-25_07.webgui', + 'url' => 'https://docs.netgate.com/downloads/pfSense-SA-25_07.webgui.asc', ], + ], + 'basedir' => '/', + 'pathstrip' => 2, + 'ignorewhitespace' => true, +], [ 'uniqid' => '0d40856c780520b97f618252d9dfb0e6719e1944', 'versions' => ['24.11'], @@ -246,6 +417,8 @@ $recommended_patches = [ [ 'text' => 'To apply, reboot OR restart PHP and the GUI from the console menu using option 16 then 11' ], [ 'text' => 'Redmine #11268', 'url' => 'https://redmine.pfsense.org/issues/11268', ], + [ 'text' => 'Redmine #15893', + 'url' => 'https://redmine.pfsense.org/issues/15893', ], ], 'basedir' => '/', 'pathstrip' => 2, diff --git a/sysutils/pfSense-pkg-System_Patches/pkg-plist b/sysutils/pfSense-pkg-System_Patches/pkg-plist index 4b6617d10f8f..d18fb84cdc7b 100644 --- a/sysutils/pfSense-pkg-System_Patches/pkg-plist +++ b/sysutils/pfSense-pkg-System_Patches/pkg-plist @@ -12,6 +12,7 @@ pkg/patches/0d40856c780520b97f618252d9dfb0e6719e1944.patch pkg/patches/0d83ed084a987f3446a0cbdcf249fc5b8722726f.patch pkg/patches/0e847960910ae9f14ca7b52c84be25fe4b9d1064.patch pkg/patches/0fc7765c886ed60555750d12808f493d70918450.patch +pkg/patches/0ff75cd0b9fb14c04c94c3585831a9f669be0a5d.patch pkg/patches/12827workaround.patch pkg/patches/12cbb18a93c1f78e05806b6d3c90511e8967f43f.patch pkg/patches/13915.patch @@ -27,6 +28,9 @@ pkg/patches/15449.patch pkg/patches/15525.patch pkg/patches/15702.patch pkg/patches/15778.patch +pkg/patches/15844-widgetkey-validation-24.11.patch +pkg/patches/15844-widgetkey-validation-ce-2.7.2.patch +pkg/patches/15856-openvpn-mgmt-fix.patch pkg/patches/16076.patch pkg/patches/16076_2.patch pkg/patches/17630ffa48e33def331a65ee50f1ba1d2c3a5de5.patch @@ -76,11 +80,13 @@ pkg/patches/57e299906c4525bcc89c728a6246495369178023.patch pkg/patches/5841d5850265476100b719e60e38b65887cd1460.patch pkg/patches/587dcb6ae708d144023879b509a1da9dd5dd0723.patch pkg/patches/5eb325e0def04241684b4d2c9583f8ca9d8bc05e.patch -pkg/patches/6bf3e080f56facab1f00e29acd24dff62d5bd707.patch pkg/patches/6021c3e059885ce3fff09e5b00df037db034ff14.patch pkg/patches/611de84ae2d1e65217dbccb7e9db32a019bd8d97.patch pkg/patches/66b989e824042c0cf5e75b1cb245b0ae13548949.patch pkg/patches/68ed289190a383795194d3499728a0f6023f8a52.patch +pkg/patches/6a92af14584d22f077e1421e952674f880cd5b6c-2_7_2.patch +pkg/patches/6a92af14584d22f077e1421e952674f880cd5b6c.patch +pkg/patches/6bf3e080f56facab1f00e29acd24dff62d5bd707.patch pkg/patches/6dc0750874b69373f8adbae9b0af223af13f5f4a.patch pkg/patches/6df70417029defed162b539720e8baa03984f653.patch pkg/patches/6f59a7f9fdfe3703667819fcbbd8b6f8cbec0d9f.patch @@ -121,6 +127,8 @@ pkg/patches/a99c03dde3df7053747ec61607c0fb2e2c0a7d22.patch pkg/patches/a9bdbd97984ff2ddefbceb2fe062fbe3a1c42d88.patch pkg/patches/aac5bb5d396a1f1b18d59a532ad262a4d1085a40.patch pkg/patches/aad64829622356cd761062e57f4a8224d1b145e4.patch +pkg/patches/acb-update-24.11.patch +pkg/patches/acb-update-ce-2.7.2.patch pkg/patches/afcc0e9c97c1993ae6b95f886665fcb4375d26c7.patch pkg/patches/b2fce958b8a2e80754b49eb3d17795757df6a76c.patch pkg/patches/b5360f49fb3c1fdc36ebf13c20b68d4ff1e15fe6.patch @@ -147,6 +155,7 @@ pkg/patches/d9fa4584e3fb63d6051e9f1db7655f931cb1be19.patch pkg/patches/dcb4461336de2fe69ac173787c8bce66e93ce672.patch pkg/patches/dcdb461733044d274c742832097b13a312045f37.patch pkg/patches/ddb57f79e26e97e2a22f701016fc70a7d1c09ce4.patch +pkg/patches/dfc70e51d556d8c1724bfc7f1fd5fe4b73faab3b.patch pkg/patches/e53f0573d853325dfb463eab8bfe59a9f4d6ce61.patch pkg/patches/e573756c98d1181583fee0f7d818c6be0ea3da0e.patch pkg/patches/e930812c680fa4adfc8d9330bec871ef57e1d1d5.patch