Version in base suite: 1.2.16+ds1-2+deb11u2 Base version: cacti_1.2.16+ds1-2+deb11u2 Target version: cacti_1.2.16+ds1-2+deb11u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/c/cacti/cacti_1.2.16+ds1-2+deb11u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/c/cacti/cacti_1.2.16+ds1-2+deb11u3.dsc changelog | 9 patches/CVE-2023-39360.patch | 27 patches/CVE-2023-39513.patch | 704 ++++++++++++++++ patches/CVE-2023-49084.patch | 47 + patches/CVE-2023-49085.patch | 32 patches/CVE-2023-49086.patch | 49 + patches/CVE-2023-49088.patch | 1857 +++++++++++++++++++++++++++++++++++++++++++ patches/series | 7 8 files changed, 2732 insertions(+) diff -Nru cacti-1.2.16+ds1/debian/changelog cacti-1.2.16+ds1/debian/changelog --- cacti-1.2.16+ds1/debian/changelog 2023-10-27 20:31:19.000000000 +0000 +++ cacti-1.2.16+ds1/debian/changelog 2024-03-15 09:33:43.000000000 +0000 @@ -1,3 +1,12 @@ +cacti (1.2.16+ds1-2+deb11u3) bullseye-security; urgency=high + + * Non-maintainer upload by the LTS Security Team. + * Fix patch for CVE-2023-39513. + * Backport security patches: CVE-2023-39360, CVE-2023-49084, + CVE-2023-49085, CVE-2023-49086, CVE-2023-49088 (Closes: #1059254) + + -- Sylvain Beucler Fri, 15 Mar 2024 10:33:43 +0100 + cacti (1.2.16+ds1-2+deb11u2) bullseye-security; urgency=high * Backport security patches from 1.2.25: CVE-2023-39357, CVE-2023-39359, diff -Nru cacti-1.2.16+ds1/debian/patches/CVE-2023-39360.patch cacti-1.2.16+ds1/debian/patches/CVE-2023-39360.patch --- cacti-1.2.16+ds1/debian/patches/CVE-2023-39360.patch 1970-01-01 00:00:00.000000000 +0000 +++ cacti-1.2.16+ds1/debian/patches/CVE-2023-39360.patch 2024-02-20 15:41:20.000000000 +0000 @@ -0,0 +1,27 @@ +Origin: https://github.com/cacti/cacti/commit/bc6dc996745ef0dee3427178c8d87a6402f3fefa +Reviewed-by: Sylvain Beucler +Last-Update: 2024-02-20 + +From bc6dc996745ef0dee3427178c8d87a6402f3fefa Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Fri, 4 Aug 2023 15:10:57 -0400 +Subject: [PATCH] QA: Different approach to XSS issue + +--- + graphs_new.php | 4 ++-- + lib/html_form.php | 2 +- + 2 files changed, 3 insertions(+), 3 deletions(-) + +Index: cacti-1.2.16+ds1/lib/html_form.php +=================================================================== +--- cacti-1.2.16+ds1.orig/lib/html_form.php ++++ cacti-1.2.16+ds1/lib/html_form.php +@@ -1308,7 +1308,7 @@ function form_save_button($cancel_url, $ + } + + if ($force_type != 'import' && $force_type != 'export' && $force_type != 'save' && $force_type != 'close' && $cancel_url != '') { +- $cancel_action = ""; ++ $cancel_action = ""; + } else { + $cancel_action = ''; + } diff -Nru cacti-1.2.16+ds1/debian/patches/CVE-2023-39513.patch cacti-1.2.16+ds1/debian/patches/CVE-2023-39513.patch --- cacti-1.2.16+ds1/debian/patches/CVE-2023-39513.patch 1970-01-01 00:00:00.000000000 +0000 +++ cacti-1.2.16+ds1/debian/patches/CVE-2023-39513.patch 2024-03-15 09:33:43.000000000 +0000 @@ -0,0 +1,704 @@ +Origin: https://github.com/cacti/cacti/commit/f66ed84ee2dfd22581e831db97afd2bb145312ef +Origin: https://github.com/cacti/cacti/commit/23abb0e0a9729bd056b56f4fb5a6fc8e7ebda523 +Reviewed-by: Sylvain Beucler +Last-Update: 2024-03-15 + +From f66ed84ee2dfd22581e831db97afd2bb145312ef Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Sun, 4 Jun 2023 12:28:26 -0400 +Subject: [PATCH] Fixing #5324 - Over Escaping Debug log + +This is an issue between releases due to escaping log entries in the wrong location in the security fix. This change resolves that issue. + +Reindex device from GUI - debug info broken due to over escaping +--- + CHANGELOG | 1 + + graphs.php | 4 ++-- + host.php | 1 + + lib/data_query.php | 58 +++++++++++++++++++++++----------------------- + lib/functions.php | 11 +++++---- + lib/template.php | 12 +++++----- + 6 files changed, 45 insertions(+), 42 deletions(-) + +From 23abb0e0a9729bd056b56f4fb5a6fc8e7ebda523 Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Sat, 17 Jun 2023 18:01:26 -0400 +Subject: [PATCH] QA: Minor rework to #5324 - XSS Issues + +Missed a few untranslated strings in addition, ensured bot the log insert and the query_debug_timer functions were covered. +--- + lib/data_query.php | 146 ++++++++++++++++++++++----------------------- + lib/functions.php | 3 +- + 2 files changed, 75 insertions(+), 74 deletions(-) + +Index: cacti-1.2.16+ds1/lib/data_query.php +=================================================================== +--- cacti-1.2.16+ds1.orig/lib/data_query.php ++++ cacti-1.2.16+ds1/lib/data_query.php +@@ -68,10 +68,10 @@ function run_data_query($host_id, $snmp_ + unset($_SESSION['debug_log']['response']); + + automation_execute_data_query($host_id, $snmp_query_id); +- query_debug_timer_offset('data_query', __('Automation Execution for Data Query complete')); ++ query_debug_timer_offset('data_query', __esc('Automation Execution for Data Query complete')); + + api_plugin_hook_function('run_data_query', array('host_id' => $host_id, 'snmp_query_id' => $snmp_query_id)); +- query_debug_timer_offset('data_query', __('Plugin Hooks complete')); ++ query_debug_timer_offset('data_query', __esc('Plugin Hooks complete')); + + return $result; + } +@@ -89,7 +89,7 @@ function run_data_query($host_id, $snmp_ + // Load the XML structure for custom settings detection + $query_array = get_data_query_array($snmp_query_id); + +- query_debug_timer_offset('data_query', __('Running Data Query [%s].', $snmp_query_id)); ++ query_debug_timer_offset('data_query', __esc('Running Data Query [%s].', $snmp_query_id)); + + $type_id = db_fetch_cell_prepared('SELECT data_input.type_id + FROM snmp_query +@@ -99,7 +99,7 @@ function run_data_query($host_id, $snmp_ + array($snmp_query_id)); + + if (isset($input_types[$type_id])) { +- query_debug_timer_offset('data_query', __('Found Type = \'%s\' [%s].', $type_id, $input_types[$type_id])); ++ query_debug_timer_offset('data_query', __esc('Found Type = \'%s\' [%s].', $type_id, $input_types[$type_id])); + } + + if ($type_id == DATA_INPUT_TYPE_SNMP_QUERY) { +@@ -121,7 +121,7 @@ function run_data_query($host_id, $snmp_ + if (isset($arguments['result']) && $arguments['result'] !== false) { + $result = $arguments['result']; + } else { +- query_debug_timer_offset('data_query', __('Unknown Type = \'%s\'.', $type_id)); ++ query_debug_timer_offset('data_query', __esc('Unknown Type = \'%s\'.', $type_id)); + } + } + +@@ -156,7 +156,7 @@ function run_data_query($host_id, $snmp_ + if (query_check_suitable($new_sort_field, $old_sort_field, $host_id, $snmp_query_id)) { + if ($old_sort_field != $new_sort_field) { + if ($old_sort_field != '') { +- query_debug_timer_offset('data_query', __('WARNING: Sort Field Association has Changed. Re-mapping issues may occur!')); ++ query_debug_timer_offset('data_query', __esc('WARNING: Sort Field Association has Changed. Re-mapping issues may occur!')); + + cacti_log('WARNING: Sort Field has Changed for Device[' . $host_id . '] and DQ[' . $snmp_query_id . ']. Old Sort:' . $old_sort_field . ', New Sort:' . $new_sort_field . '. Re-mapping issues may occur!', false, 'REINDEX'); + } +@@ -168,7 +168,7 @@ function run_data_query($host_id, $snmp_ + } + } + +- query_debug_timer_offset('data_query', __('Update Data Query Sort Cache complete')); ++ query_debug_timer_offset('data_query', __esc('Update Data Query Sort Cache complete')); + + /* recalculate/change sort order */ + $local_data = db_fetch_assoc_prepared('SELECT dl.id AS local_data_id, dl.host_id, +@@ -191,7 +191,7 @@ function run_data_query($host_id, $snmp_ + $changed_ids = array(); + $orphaned_ids = array(); + if (cacti_sizeof($local_data)) { +- query_debug_timer_offset('data_query', __('Found %s Local Data ID\'s to Verify', cacti_sizeof($local_data))); ++ query_debug_timer_offset('data_query', __esc('Found %s Local Data ID\'s to Verify', cacti_sizeof($local_data))); + + foreach($local_data as $data_source) { + // Just in case there is a forced type from the data source page +@@ -285,7 +285,7 @@ function run_data_query($host_id, $snmp_ + // Non blank index found + // Check to see if the index changed + if ($current_index != $data_source['snmp_index']) { +- query_debug_timer_offset('data_query', __('Index Change Detected! CurrentIndex: %s, PreviousIndex: %s', $current_index, $data_source['query_index'])); ++ query_debug_timer_offset('data_query', __esc('Index Change Detected! CurrentIndex: %s, PreviousIndex: %s', $current_index, $data_source['query_index'])); + + db_execute_prepared('UPDATE data_local + SET snmp_index = ?, +@@ -298,10 +298,10 @@ function run_data_query($host_id, $snmp_ + } elseif ($data_source['snmp_index'] != '' && !$forced_type) { + if (isset($query_array['index_transient']) && $query_array['index_transient'] == 'true') { + // Found removed index, but this is expected, so no action taken +- query_debug_timer_offset('data_query', __('Transient Index Removal Detected! PreviousIndex: %s. No action taken.', $data_source['query_index'])); ++ query_debug_timer_offset('data_query', __esc('Transient Index Removal Detected! PreviousIndex: %s. No action taken.', $data_source['query_index'])); + } else { + // Found a deleted index, masking off to prevent issues +- query_debug_timer_offset('data_query', __('Index Removal Detected! PreviousIndex: %s', $data_source['query_index'])); ++ query_debug_timer_offset('data_query', __esc('Index Removal Detected! PreviousIndex: %s', $data_source['query_index'])); + + // Set the index to Null, note that the Data Source still has the value + db_execute_prepared('UPDATE data_local +@@ -328,45 +328,45 @@ function run_data_query($host_id, $snmp_ + } + } + +- query_debug_timer_offset('data_query', __('Verification of %s Local Data ID\'s Complete', cacti_sizeof($local_data))); ++ query_debug_timer_offset('data_query', __esc('Verification of %s Local Data ID\'s Complete', cacti_sizeof($local_data))); + + if (cacti_sizeof($changed_ids) || cacti_sizeof($orphaned_ids)) { +- query_debug_timer_offset('data_query', __('Found Changed %s and %s Orphaned Local Data ID\'s to Re-map.', cacti_sizeof($changed_ids), cacti_sizeof($orphaned_ids))); ++ query_debug_timer_offset('data_query', __esc('Found Changed %s and %s Orphaned Local Data ID\'s to Re-map.', cacti_sizeof($changed_ids), cacti_sizeof($orphaned_ids))); + data_query_remap_indexes($changed_ids); + data_query_remap_indexes($orphaned_ids); +- query_debug_timer_offset('data_query', __('Done remapping Graphs to their new Indexes')); ++ query_debug_timer_offset('data_query', __esc('Done remapping Graphs to their new Indexes')); + } + + if ((cacti_sizeof($changed_ids) || cacti_sizeof($orphaned_ids)) && !$force) { + /* update title cache for graph and data source */ + update_data_source_title_cache_from_host($host_id, $snmp_query_id, array_merge($changed_ids, $orphaned_ids)); +- query_debug_timer_offset('data_query', __('Done updating Data Source Title Cache')); ++ query_debug_timer_offset('data_query', __esc('Done updating Data Source Title Cache')); + + update_graph_title_cache_from_host($host_id, $snmp_query_id, array_merge($changed_ids, $orphaned_ids)); +- query_debug_timer_offset('data_query', __('Done updating Graph Title Cache')); ++ query_debug_timer_offset('data_query', __esc('Done updating Graph Title Cache')); + } elseif ($force) { + /* update title cache for graph and data source */ + update_data_source_title_cache_from_host($host_id, $snmp_query_id); +- query_debug_timer_offset('data_query', __('Done updating Data Source Title Cache')); ++ query_debug_timer_offset('data_query', __esc('Done updating Data Source Title Cache')); + + update_graph_title_cache_from_host($host_id, $snmp_query_id); +- query_debug_timer_offset('data_query', __('Done updating Graph Title Cache')); ++ query_debug_timer_offset('data_query', __esc('Done updating Graph Title Cache')); + } + } + +- query_debug_timer_offset('data_query', __('Index Association with Local Data complete')); ++ query_debug_timer_offset('data_query', __esc('Index Association with Local Data complete')); + + update_reindex_cache($host_id, $snmp_query_id); + + /* update the auto reindex cache */ + if (cacti_sizeof($changed_ids)) { +- query_debug_timer_offset('data_query', __('Update Re-Index Cache complete. There were ' . cacti_sizeof($changed_ids) . ' index changes, and ' . cacti_sizeof($orphaned_ids) . ' orphaned indexes.')); ++ query_debug_timer_offset('data_query', __esc('Update Re-Index Cache complete. There were %s index changes, and %s orphaned indexes.', cacti_sizeof($changed_ids), cacti_sizeof($orphaned_ids))); + + /* update the poller cache */ + update_poller_cache_from_query($host_id, $snmp_query_id, $changed_ids); +- query_debug_timer_offset('data_query', __('Update Poller Cache for Query complete')); ++ query_debug_timer_offset('data_query', __esc('Update Poller Cache for Query complete')); + } else { +- query_debug_timer_offset('data_query', __('No Index Changes Detected, Skipping Re-Index and Poller Cache Re-population')); ++ query_debug_timer_offset('data_query', __esc('No Index Changes Detected, Skipping Re-Index and Poller Cache Re-population')); + } + + if (cacti_sizeof($orphaned_ids) && +@@ -388,18 +388,18 @@ function run_data_query($host_id, $snmp_ + if ($config['poller_id'] == 1) { + /* perform any automation on reindex */ + automation_execute_data_query($host_id, $snmp_query_id); +- query_debug_timer_offset('data_query', __('Automation Executing for Data Query complete')); ++ query_debug_timer_offset('data_query', __esc('Automation Executing for Data Query complete')); + + api_plugin_hook_function('run_data_query', array('host_id' => $host_id, 'snmp_query_id' => $snmp_query_id)); +- query_debug_timer_offset('data_query', __('Plugin hooks complete')); ++ query_debug_timer_offset('data_query', __esc('Plugin hooks complete')); + } elseif ($config['connection'] == 'online') { + poller_push_reindex_data_to_poller($host_id, $snmp_query_id); + + automation_execute_data_query($host_id, $snmp_query_id); +- query_debug_timer_offset('data_query', __('Automation Execution for Data Query complete')); ++ query_debug_timer_offset('data_query', __esc('Automation Execution for Data Query complete')); + + api_plugin_hook_function('run_data_query', array('host_id' => $host_id, 'snmp_query_id' => $snmp_query_id)); +- query_debug_timer_offset('data_query', __('Plugin Hooks complete')); ++ query_debug_timer_offset('data_query', __esc('Plugin Hooks complete')); + + if (!isset($_SESSION)) { + $config['debug_log']['result'] = $result; +@@ -447,12 +447,12 @@ function data_query_remove_disabled_item + + function query_check_suitable($new_sort_field, $old_sort_field, $host_id, $snmp_query_id) { + if ($new_sort_field == $old_sort_field) { +- query_debug_timer_offset('data_query', __('Checking for Sort Field change. No changes detected.')); ++ query_debug_timer_offset('data_query', __esc('Checking for Sort Field change. No changes detected.')); + + return true; + } + +- query_debug_timer_offset('data_query', __('Detected New Sort Field: \'%s\' Old Sort Field \'%s\'', $new_sort_field, $old_sort_field)); ++ query_debug_timer_offset('data_query', __esc('Detected New Sort Field: \'%s\' Old Sort Field \'%s\'', $new_sort_field, $old_sort_field)); + + $new_sort_count = db_fetch_cell_prepared('SELECT COUNT(*) + FROM host_snmp_cache +@@ -469,7 +469,7 @@ function query_check_suitable($new_sort_ + array($host_id, $snmp_query_id, $old_sort_field)); + + if ($new_sort_count < $old_sort_count) { +- query_debug_timer_offset('data_query', __('ERROR: New Sort Field not suitable. Sort Field will not change.')); ++ query_debug_timer_offset('data_query', __esc('ERROR: New Sort Field not suitable. Sort Field will not change.')); + + /* update the cache */ + db_execute_prepared('UPDATE host_snmp_query +@@ -481,7 +481,7 @@ function query_check_suitable($new_sort_ + return false; + } + +- query_debug_timer_offset('data_query', __('New Sort Field validated. Sort Field be updated.')); ++ query_debug_timer_offset('data_query', __esc('New Sort Field validated. Sort Field be updated.')); + + return true; + } +@@ -569,11 +569,11 @@ function get_data_query_array($snmp_quer + $xml_file_path = str_replace($search, $replace, $xml_file_path); + + if (!file_exists($xml_file_path)) { +- query_debug_timer_offset('data_query', __('Could not find data query XML file at \'%s\'', $xml_file_path)); ++ query_debug_timer_offset('data_query', __esc('Could not find data query XML file at \'%s\'', $xml_file_path)); + return array(); + } + +- query_debug_timer_offset('data_query', __('Found data query XML file at \'%s\'', $xml_file_path)); ++ query_debug_timer_offset('data_query', __esc('Found data query XML file at \'%s\'', $xml_file_path)); + + $data = implode('',file($xml_file_path)); + +@@ -596,11 +596,11 @@ function query_script_host($host_id, $sn + + /* invalid xml check */ + if ((!is_array($script_queries)) || (cacti_sizeof($script_queries) == 0)) { +- query_debug_timer_offset('data_query', __('Error parsing XML file into an array.')); ++ query_debug_timer_offset('data_query', __esc('Error parsing XML file into an array.')); + return false; + } + +- query_debug_timer_offset('data_query', __('XML file parsed ok.')); ++ query_debug_timer_offset('data_query', __esc('XML file parsed ok.')); + + /* are we talking to script server? */ + if (isset($script_queries['script_server'])) { +@@ -626,14 +626,14 @@ function query_script_host($host_id, $sn + + // if the number of indexes does not exist use emulation + if (!cacti_sizeof($script_num_index_array)) { +- query_debug_timer_offset('data_query', __('Data Query returned no indexes.')); ++ query_debug_timer_offset('data_query', __esc('Data Query returned no indexes.')); + query_debug_timer_offset('data_query', __('<arg_num_indexes> exists in XML file but no data returned., \'Index Count Changed\' not supported')); + } + +- query_debug_timer_offset('data_query', __('Executing script for num of indexes \'%s\'', $script_path)); ++ query_debug_timer_offset('data_query', __esc('Executing script for num of indexes \'%s\'', $script_path)); + + foreach ($script_num_index_array as $element) { +- query_debug_timer_offset('data_query', __('Found number of indexes: %s' , $element)); ++ query_debug_timer_offset('data_query', __esc('Found number of indexes: %s' , $element)); + } + } else { + if (isset($script_queries['script_server'])) { +@@ -650,16 +650,16 @@ function query_script_host($host_id, $sn + $script_index_array = exec_into_array($script_path); + + if (!cacti_sizeof($script_index_array)) { +- query_debug_timer_offset('data_query', __('ERROR: Data Query returned no indexes.')); ++ query_debug_timer_offset('data_query', __esc('ERROR: Data Query returned no indexes.')); + return false; + } + +- query_debug_timer_offset('data_query', __('Executing script for list of indexes \'%s\', Index Count: %s', $script_path, cacti_sizeof($script_index_array))); ++ query_debug_timer_offset('data_query', __esc('Executing script for list of indexes \'%s\', Index Count: %s', $script_path, cacti_sizeof($script_index_array))); + +- debug_log_insert_section_start('data_query', __('Click to show Data Query output for \'index\''), true); ++ debug_log_insert_section_start('data_query', __esc('Click to show Data Query output for \'index\''), true); + + foreach ($script_index_array as $element) { +- debug_log_insert('data_query', __('Found index: %s', $element)); ++ debug_log_insert('data_query', __esc('Found index: %s', $element)); + } + + debug_log_insert_section_end('data_query'); +@@ -674,15 +674,15 @@ function query_script_host($host_id, $sn + $rewrite_value = isset($field_array['rewrite_value']) ? $field_array['rewrite_value'] : null; + $script_path = get_script_query_path((isset($script_queries['arg_prepend']) ? $script_queries['arg_prepend'] . ' ': '') . $script_queries['arg_query'] . ' ' . $field_array['query_name'], $script_queries['script_path'], $host_id); + +- debug_log_insert_section_start('data_query', __('Click to show Data Query output for field \'%s\'', $field_name), true); ++ debug_log_insert_section_start('data_query', __esc('Click to show Data Query output for field \'%s\'', $field_name), true); + + $script_data_array = exec_into_array($script_path); + + if (!cacti_sizeof($script_data_array) && $field_name == $sort_field) { + $empty_types[] = $field_name; +- query_debug_timer_offset('data_query', __('Sort field returned no data for field name %s, skipping', $field_name)); ++ query_debug_timer_offset('data_query', __esc('Sort field returned no data for field name %s, skipping', $field_name)); + } else { +- debug_log_insert('data_query', __('Executing script query \'%s\'', $script_path)); ++ debug_log_insert('data_query', __esc('Executing script query \'%s\'', $script_path)); + + if (cacti_sizeof($script_data_array)) { + foreach ($script_data_array as $element) { +@@ -692,7 +692,7 @@ function query_script_host($host_id, $sn + + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $field_value, $script_index, ''); + +- debug_log_insert('data_query', __('Found item [%s=\'%s\'] index: %s', $field_name, $field_value, $script_index)); ++ debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s', $field_name, $field_value, $script_index)); + } + } + } +@@ -770,13 +770,13 @@ function query_snmp_host($host_id, $snmp + $sort_field = get_best_data_query_index_type($host_id, $snmp_query_id); + + if (!cacti_sizeof($host) || $host['hostname'] == '') { +- query_debug_timer_offset('data_query', __('Invalid host_id: %s', $host_id)); ++ query_debug_timer_offset('data_query', __esc('Invalid host_id: %s', $host_id)); + return false; + } + + /* invalid xml check */ + if ((!is_array($snmp_queries)) || (cacti_sizeof($snmp_queries) == 0)) { +- query_debug_timer_offset('data_query', __('Error parsing XML file into an array.')); ++ query_debug_timer_offset('data_query', __esc('Error parsing XML file into an array.')); + return false; + } + +@@ -795,7 +795,7 @@ function query_snmp_host($host_id, $snmp + $host['snmp_timeout'], $host['ping_retries'], $host['max_oids']); + + if ($session === false) { +- debug_log_insert('data_query', __('Failed to load SNMP session.')); ++ debug_log_insert('data_query', __esc('Failed to load SNMP session.')); + + return false; + } +@@ -804,7 +804,7 @@ function query_snmp_host($host_id, $snmp + if (isset($snmp_queries['oid_num_indexes'])) { + $snmp_num_indexes = cacti_snmp_session_get($session, $snmp_queries['oid_num_indexes']); + +- query_debug_timer_offset('data_query', __('Executing SNMP get for num of indexes @ \'%s\' Index Count: %s' , $snmp_queries['oid_num_indexes'] , $snmp_num_indexes)); ++ query_debug_timer_offset('data_query', __esc('Executing SNMP get for num of indexes @ \'%s\' Index Count: %s' , $snmp_queries['oid_num_indexes'] , $snmp_num_indexes)); + } else { + query_debug_timer_offset('data_query', __('<oid_num_indexes> missing in XML file, \'Index Count Changed\' emulated by counting oid_index entries')); + } +@@ -812,16 +812,16 @@ function query_snmp_host($host_id, $snmp + /* fetch specified index at specified OID */ + $snmp_indexes = cacti_snmp_session_walk($session, $snmp_queries['oid_index']); + +- query_debug_timer_offset('data_query', __('Executing SNMP walk for list of indexes @ \'%s\' Index Count: %s', $snmp_queries['oid_index'] , cacti_sizeof($snmp_indexes))); ++ query_debug_timer_offset('data_query', __esc('Executing SNMP walk for list of indexes @ \'%s\' Index Count: %s', $snmp_queries['oid_index'] , cacti_sizeof($snmp_indexes))); + + /* no data found; get out */ + if (!cacti_sizeof($snmp_indexes)) { +- query_debug_timer_offset('data_query', __('No SNMP data returned')); ++ query_debug_timer_offset('data_query', __esc('No SNMP data returned')); + return false; + } else { + /* show list of indices found */ + foreach ($snmp_indexes as $oid => $value) { +- query_debug_timer_offset('data_query', __('Index found at OID: \'%s\' value: \'%s\'', $oid , $value)); ++ query_debug_timer_offset('data_query', __esc('Index found at OID: \'%s\' value: \'%s\'', $oid , $value)); + } + } + +@@ -840,11 +840,11 @@ function query_snmp_host($host_id, $snmp + } + + $snmp_indexes = $parsed_indexes; +- query_debug_timer_offset('data_query', __('Filtering list of indexes @ \'%s\' Index Count: %s', $snmp_queries['oid_index'] , cacti_sizeof($snmp_indexes))); ++ query_debug_timer_offset('data_query', __esc('Filtering list of indexes @ \'%s\' Index Count: %s', $snmp_queries['oid_index'] , cacti_sizeof($snmp_indexes))); + + /* show list of indices found */ + foreach ($snmp_indexes as $oid => $value) { +- query_debug_timer_offset('data_query', __('Filtered Index found at OID: \'%s\' value: \'%s\'', $oid , $value)); ++ query_debug_timer_offset('data_query', __esc('Filtered Index found at OID: \'%s\' value: \'%s\'', $oid , $value)); + } + } + +@@ -864,19 +864,19 @@ function query_snmp_host($host_id, $snmp + if ($field_array['source'] != 'index' && ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output') && $field_array['method'] != 'get' && + (isset($field_array['rewrite_index']) || isset($field_array['oid_suffix']))) { + $field_array['method'] = 'get'; +- debug_log_insert('data_query', __('Fixing wrong \'method\' field for \'%s\' since \'rewrite_index\' or \'oid_suffix\' is defined',$field_name)); ++ debug_log_insert('data_query', __esc('Fixing wrong \'method\' field for \'%s\' since \'rewrite_index\' or \'oid_suffix\' is defined',$field_name)); + } + + $rewrite_value = isset($field_array['rewrite_value']) ? $field_array['rewrite_value'] : null; + + if ((!isset($field_array['oid'])) && ($field_array['source'] == 'index')) { + foreach ($snmp_indexes as $oid => $value) { +- query_debug_timer_offset('data_query', __('Inserting index data for field \'%s\' [value=\'%s\']' , $field_name, $value)); ++ query_debug_timer_offset('data_query', __esc('Inserting index data for field \'%s\' [value=\'%s\']' , $field_name, $value)); + + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $value, $value, ''); + } + } elseif (($field_array['method'] == 'get') && ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output')) { +- query_debug_timer_offset('data_query', __('Located input field \'%s\' [get]',$field_name)); ++ query_debug_timer_offset('data_query', __esc('Located input field \'%s\' [get]',$field_name)); + + if ($field_array['source'] == 'value' && !isset($field_array['rewrite_index'])) { + $oid_rewrite_pattern = null; +@@ -885,7 +885,7 @@ function query_snmp_host($host_id, $snmp + if (isset($field_array['oid_rewrite_pattern']) && isset($field_array['oid_rewrite_replacement'])) { + $oid_rewrite_pattern = '/' . str_replace('OID/REGEXP:', '', $field_array['oid_rewrite_pattern']) . '/'; + $oid_rewrite_replacement = $field_array['oid_rewrite_replacement']; +- query_debug_timer_offset('data_query', __('Found OID rewrite rule: \'s/%s/%s/\'', $oid_rewrite_pattern,$oid_rewrite_replacement)); ++ query_debug_timer_offset('data_query', __esc('Found OID rewrite rule: \'s/%s/%s/\'', $oid_rewrite_pattern,$oid_rewrite_replacement)); + } + + foreach ($snmp_indexes as $oid => $index) { +@@ -896,7 +896,7 @@ function query_snmp_host($host_id, $snmp + if (isset($oid_rewrite_pattern)) { + $orig_oid = $oid; + $oid = preg_replace($oid_rewrite_pattern, $oid_rewrite_replacement, $oid); +- query_debug_timer_offset('data_query', __('oid_rewrite at OID: \'%s\' new OID: \'%s\'', $orig_oid , $oid)); ++ query_debug_timer_offset('data_query', __esc('oid_rewrite at OID: \'%s\' new OID: \'%s\'', $orig_oid , $oid)); + } + + if (isset($field_array['output_format'])) { +@@ -917,7 +917,7 @@ function query_snmp_host($host_id, $snmp + $value = cacti_snmp_session_get($session, $oid); + } + +- query_debug_timer_offset('data_query', __('Executing SNMP get for data @ \'%s\' [value=\'%s\']', $oid, $value)); ++ query_debug_timer_offset('data_query', __esc('Executing SNMP get for data @ \'%s\' [value=\'%s\']', $oid, $value)); + + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $value, $index, $oid); + } +@@ -928,7 +928,7 @@ function query_snmp_host($host_id, $snmp + $rewritten_indexes = data_query_rewrite_indexes($errmsg, $host_id, $snmp_query_id, $field_array['rewrite_index'], $snmp_indexes, $fields_processed); + if (cacti_sizeof($errmsg)) { + foreach ($errmsg as $message) { +- debug_log_insert('data_query', __('Field \'%s\' %s', $field_name,$message)); ++ debug_log_insert('data_query', __esc('Field \'%s\' %s', $field_name,$message)); + } + } + } +@@ -964,7 +964,7 @@ function query_snmp_host($host_id, $snmp + $oids[] = $value['oid']; + } + +- debug_log_insert('data_query', __('Executing SNMP get for %s oids (%s)' , cacti_count($oids), implode(', ', $oids))); ++ debug_log_insert('data_query', __esc('Executing SNMP get for %s oids (%s)' , cacti_count($oids), implode(', ', $oids))); + + $value_output_format = SNMP_STRING_OUTPUT_GUESS; + if (isset($field_array['output_format'])) { +@@ -990,11 +990,11 @@ function query_snmp_host($host_id, $snmp + } + + if (!cacti_sizeof($results) && $field_name == $sort_field) { +- query_debug_timer_offset('data_query', __('Sort field returned no data for OID[%s], skipping.', $oid)); ++ query_debug_timer_offset('data_query', __esc('Sort field returned no data for OID[%s], skipping.', $oid)); + } elseif (cacti_sizeof($results)) { + foreach ($results as $key => $value) { + debug_log_insert('data_query', +- __('Found result for data @ \'%s\' [value=\'%s\']', ++ __esc('Found result for data @ \'%s\' [value=\'%s\']', + $key, $value)); + } + +@@ -1002,11 +1002,11 @@ function query_snmp_host($host_id, $snmp + if (isset($results[$values[$key]['oid']])) { + $values[$key]['value'] = $results[$values[$key]['oid']]; + debug_log_insert('data_query', +- __('Setting result for data @ \'%s\' [key=\'%s\', value=\'%s\']', ++ __esc('Setting result for data @ \'%s\' [key=\'%s\', value=\'%s\']', + $values[$key]['oid'], $key, $values[$key]['value'])); + } else { + debug_log_insert('data_query', +- __('Skipped result for data @ \'%s\' [key=\'%s\', value=\'%s\']', ++ __esc('Skipped result for data @ \'%s\' [key=\'%s\', value=\'%s\']', + $values[$key]['oid'], $key, $values[$key]['value'])); + } + } +@@ -1020,7 +1020,7 @@ function query_snmp_host($host_id, $snmp + } + + foreach ($values as $item) { +- debug_log_insert('data_query', __('Got SNMP get result for data @ \'%s\' [value=\'%s\'] (index: %s)', $item['oid'], $item['value'], $item['index'])); ++ debug_log_insert('data_query', __esc('Got SNMP get result for data @ \'%s\' [value=\'%s\'] (index: %s)', $item['oid'], $item['value'], $item['index'])); + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $item['value'], $item['index'], $item['oid']); + } + +@@ -1050,15 +1050,15 @@ function query_snmp_host($host_id, $snmp + + $value = preg_replace('/' . str_replace('VALUE/REGEXP:', '', $field_array['source']) . '/', "\\1", $value); + +- query_debug_timer_offset('data_query', __('Executing SNMP get for data @ \'%s\' [value=\'$value\']', $oid, $value)); ++ query_debug_timer_offset('data_query', __esc('Executing SNMP get for data @ \'%s\' [value=\'$value\']', $oid, $value)); + + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $value, $index, $oid); + } + } + } elseif ($field_array['method'] == 'walk' && ($field_array['direction'] == 'input' || $field_array['direction'] == 'input-output')) { +- debug_log_insert_section_start('data_query', __('Click to show Data Query output for field \'%s\'', $field_name), true); ++ debug_log_insert_section_start('data_query', __esc('Click to show Data Query output for field \'%s\'', $field_name), true); + +- query_debug_timer_offset('data_query', __('Located input field \'%s\' [walk]', $field_name)); ++ query_debug_timer_offset('data_query', __esc('Located input field \'%s\' [walk]', $field_name)); + + if (isset($field_array['output_format'])) { + if ($field_array['output_format'] == 'hex') { +@@ -1082,7 +1082,7 @@ function query_snmp_host($host_id, $snmp + } + } elseif ($field_name == $sort_field) { + $empty_types[] = $field_name; +- query_debug_timer_offset('data_query', __('Sort field returned no data for OID[%s], skipping.', $field_array['oid'])); ++ query_debug_timer_offset('data_query', __esc('Sort field returned no data for OID[%s], skipping.', $field_array['oid'])); + continue; + } + } else { +@@ -1090,12 +1090,12 @@ function query_snmp_host($host_id, $snmp + + if (!cacti_sizeof($snmp_data) && $field_name == $sort_field) { + $empty_types[] = $field_name; +- query_debug_timer_offset('data_query', __('Sort field returned no data for OID[%s], skipping.', $field_array['oid'])); ++ query_debug_timer_offset('data_query', __esc('Sort field returned no data for OID[%s], skipping.', $field_array['oid'])); + continue; + } + } + +- query_debug_timer_offset('data_query', __('Executing SNMP walk for data @ \'%s\'', $field_array['oid'])); ++ query_debug_timer_offset('data_query', __esc('Executing SNMP walk for data @ \'%s\'', $field_array['oid'])); + + if (preg_match('/^VALUE\/TABLE:(.*)/',$field_array['source'],$matches)) { + preg_match_all('/([^:]+):([^:]+)/',$matches[1],$match_temp); +@@ -1146,7 +1146,7 @@ function query_snmp_host($host_id, $snmp + } + + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, isset($modified_value)?$modified_value:$value , $snmp_index, $oid); +- debug_log_insert('data_query', __('Found item [%s=\'%s\'] index: %s [from %s]',$field_name,isset($modified_value)?"$modified_value ($value)":$value,$snmp_index,$mode)); ++ debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s [from %s]',$field_name,isset($modified_value)?"$modified_value ($value)":$value,$snmp_index,$mode)); + unset($modified_value); + } + } +@@ -1180,13 +1180,13 @@ function query_snmp_host($host_id, $snmp + } + } + if ($isascii) { +- query_debug_timer_offset('data_query', __('Found OCTET STRING \'%s\' decoded value: \'%s\'', $parse_value, $decoded)); ++ query_debug_timer_offset('data_query', __esc('Found OCTET STRING \'%s\' decoded value: \'%s\'', $parse_value, $decoded)); + $parse_value = $decoded; + } + } + } + +- debug_log_insert('data_query', __('Found item [%s=\'%s\'] index: %s [from regexp oid parse]', $field_name, $parse_value, $snmp_index)); ++ debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s [from regexp oid parse]', $field_name, $parse_value, $snmp_index)); + + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $parse_value, $snmp_index, $oid); + } +@@ -1206,7 +1206,7 @@ function query_snmp_host($host_id, $snmp + + $oid = $field_array['oid']; + +- debug_log_insert('data_query', __('Found item [%s=\'%s\'] index: %s [from regexp oid value parse]', $field_name, $parse_value, $snmp_index)); ++ debug_log_insert('data_query', __esc('Found item [%s=\'%s\'] index: %s [from regexp oid value parse]', $field_name, $parse_value, $snmp_index)); + + $output_array[] = data_query_format_record($host_id, $snmp_query_id, $field_name, $rewrite_value, $parse_value, $snmp_index, $oid); + } +@@ -1427,7 +1427,7 @@ function rewrite_snmp_enum_value($field_ + $newmap = array(); + foreach ($map as $index => $item) { + if (!isset($item['match']) || !isset($item['replace'])) { +- debug_log_insert('data_query', "Bogus rewrite_value item found, index='$index'"); ++ debug_log_insert('data_query', __esc('Bogus rewrite_value item found, index=\'%s\'', $index)); + continue; + } + +@@ -1440,7 +1440,7 @@ function rewrite_snmp_enum_value($field_ + } + + if ($map === false || !is_array($map)) { +- debug_log_insert('data_query', 'Could not parse translation map (rewrite_value)'); ++ debug_log_insert('data_query', __esc('Could not parse translation map (rewrite_value)')); + return $value; + } + +@@ -1464,7 +1464,7 @@ function rewrite_snmp_enum_value($field_ + foreach ($mapcache[$field_name] as $src => $dst) { + if (preg_match($src, $value)) { + $nvalue = preg_replace($src, $dst, $value); +- debug_log_insert('data_query', "rewrite_value: '$value' => '$nvalue'"); ++ debug_log_insert('data_query', __esc('rewrite_value: \'%s\' => \'%s\'', $value, $nvalue)); + $value = $nvalue; + break; + } +@@ -1569,7 +1569,7 @@ function update_data_query_cache($host_i + } + } + +- query_debug_timer_offset('data_query', 'Update graph data query cache complete'); ++ query_debug_timer_offset('data_query', __esc('Update graph data query cache complete')); + + $data_sources = db_fetch_assoc_prepared('SELECT * + FROM data_local +@@ -1586,7 +1586,7 @@ function update_data_query_cache($host_i + } + } + +- query_debug_timer_offset('data_query', __('Re-Indexing Data Query complete')); ++ query_debug_timer_offset('data_query', __esc('Re-Indexing Data Query complete')); + } + + /* update_graph_data_query_cache - updates the local data query cache for a particular +@@ -2151,7 +2151,7 @@ function verify_index_order($raw_xml) { + + /* invalid xml check */ + if ((!is_array($raw_xml)) || (cacti_sizeof($raw_xml) == 0)) { +- query_debug_timer_offset('data_query', __('Error parsing XML file into an array.')); ++ query_debug_timer_offset('data_query', __esc('Error parsing XML file into an array.')); + return false; + } + +Index: cacti-1.2.16+ds1/lib/functions.php +=================================================================== +--- cacti-1.2.16+ds1.orig/lib/functions.php ++++ cacti-1.2.16+ds1/lib/functions.php +@@ -3035,6 +3035,9 @@ function debug_log_clear($type = '') { + } + + /* debug_log_return - returns the debug log for a particular category ++ ++ NOTE: Escaping is done in the insert functions. ++ + @arg $type - the 'category' to return the debug log for. + @returns - the full debug log for a particular category */ + function debug_log_return($type) { +Index: cacti-1.2.16+ds1/graphs.php +=================================================================== +--- cacti-1.2.16+ds1.orig/graphs.php ++++ cacti-1.2.16+ds1/graphs.php +@@ -249,7 +249,7 @@ function form_save() { + $return_array = create_complete_graph_from_template($graph_template_id, $host_id, $snmp_query_array, $suggested_values); + + if ($return_array !== false) { +- debug_log_insert('new_graphs', __('Created graph: %s', get_graph_title($return_array['local_graph_id']))); ++ debug_log_insert('new_graphs', __esc('Created graph: %s', get_graph_title($return_array['local_graph_id']))); + + /* lastly push host-specific information to our data sources */ + if (cacti_sizeof($return_array['local_data_id'])) { # we expect at least one data source associated +@@ -257,7 +257,7 @@ function form_save() { + push_out_host($host_id, $item); + } + } else { +- debug_log_insert('new_graphs', __('ERROR: No Data Source associated. Check Template')); ++ debug_log_insert('new_graphs', __esc('ERROR: No Data Source associated. Check Template')); + } + } + +Index: cacti-1.2.16+ds1/lib/template.php +=================================================================== +--- cacti-1.2.16+ds1.orig/lib/template.php ++++ cacti-1.2.16+ds1/lib/template.php +@@ -1649,10 +1649,10 @@ function create_save_graph($host_id, $fo + push_out_host($host_id, $item); + } + } else { +- debug_log_insert('new_graphs', __('ERROR: No Data Source associated. Check Template')); ++ debug_log_insert('new_graphs', __esc('ERROR: No Data Source associated. Check Template')); + } + } else { +- debug_log_insert('new_graphs', __('ERROR: Whitelist Validation Failed. Check Data Input Method')); ++ debug_log_insert('new_graphs', __esc('ERROR: Whitelist Validation Failed. Check Data Input Method')); + } + } elseif ($form_type == 'sg') { + foreach ($snmp_index_array as $snmp_index => $true) { +@@ -1669,10 +1669,10 @@ function create_save_graph($host_id, $fo + push_out_host($host_id, $item); + } + } else { +- debug_log_insert('new_graphs', __('ERROR: No Data Source associated. Check Template')); ++ debug_log_insert('new_graphs', __esc('ERROR: No Data Source associated. Check Template')); + } + } else { +- debug_log_insert('new_graphs', __('ERROR: Whitelist Validation Failed. Check Data Input Method')); ++ debug_log_insert('new_graphs', __esc('ERROR: Whitelist Validation Failed. Check Data Input Method')); + } + } + } diff -Nru cacti-1.2.16+ds1/debian/patches/CVE-2023-49084.patch cacti-1.2.16+ds1/debian/patches/CVE-2023-49084.patch --- cacti-1.2.16+ds1/debian/patches/CVE-2023-49084.patch 1970-01-01 00:00:00.000000000 +0000 +++ cacti-1.2.16+ds1/debian/patches/CVE-2023-49084.patch 2024-03-14 17:29:33.000000000 +0000 @@ -0,0 +1,47 @@ +Origin: https://github.com/cacti/cacti/commit/5f451bc680d7584525d18026836af2a1e31b2188 +Origin: https://github.com/cacti/cacti/commit/c3a647e9867ae8e2982e26342630ba9edb2d94b7 +Reviewed-by: Sylvain Beucler +Last-Update: 2024-03-14 + +From 5f451bc680d7584525d18026836af2a1e31b2188 Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Sat, 18 Nov 2023 10:03:08 -0500 +Subject: [PATCH] QA: Increase Cacti Security in four areas + +--- + link.php | 2 +- + pollers.php | 4 ++-- + reports_admin.php | 10 +++++----- + reports_user.php | 10 +++++----- + 4 files changed, 13 insertions(+), 13 deletions(-) + +From c3a647e9867ae8e2982e26342630ba9edb2d94b7 Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Wed, 22 Nov 2023 07:46:14 -0500 +Subject: [PATCH] QA: Further hardening of External Links + +Attribution: Aleksey Solovev (Positive Technologies) +Advisory forthcoming. +--- + link.php | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +Index: cacti-1.2.16+ds1/link.php +=================================================================== +--- cacti-1.2.16+ds1.orig/link.php ++++ cacti-1.2.16+ds1/link.php +@@ -76,10 +76,11 @@ if (!cacti_sizeof($page)) { + } else { + print '
'; + +- $file = $config['base_path'] . "/include/content/" . str_replace('../', '', $page['contentfile']); ++ $basepath = $config['base_path'] . '/include/content'; ++ $file = realpath($basepath . '/' . $page['contentfile']); + +- if (file_exists($file)) { +- include_once($file); ++ if ($file !== false && substr($file, 0, strlen($basepath)) == $basepath) { ++ print file_get_contents($file); + } else { + print '

The file \'' . html_escape($page['contentfile']) . '\' does not exist!!

'; + } diff -Nru cacti-1.2.16+ds1/debian/patches/CVE-2023-49085.patch cacti-1.2.16+ds1/debian/patches/CVE-2023-49085.patch --- cacti-1.2.16+ds1/debian/patches/CVE-2023-49085.patch 1970-01-01 00:00:00.000000000 +0000 +++ cacti-1.2.16+ds1/debian/patches/CVE-2023-49085.patch 2024-03-14 17:25:15.000000000 +0000 @@ -0,0 +1,32 @@ +Origin: https://github.com/cacti/cacti/commit/5f451bc680d7584525d18026836af2a1e31b2188 +Reviewed-by: Sylvain Beucler +Last-Update: 2024-02-26 + +From 5f451bc680d7584525d18026836af2a1e31b2188 Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Sat, 18 Nov 2023 10:03:08 -0500 +Subject: [PATCH] QA: Increase Cacti Security in four areas + +--- + link.php | 2 +- + pollers.php | 4 ++-- + reports_admin.php | 10 +++++----- + reports_user.php | 10 +++++----- + 4 files changed, 13 insertions(+), 13 deletions(-) + +Index: cacti-1.2.16+ds1/pollers.php +=================================================================== +--- cacti-1.2.16+ds1.orig/pollers.php ++++ cacti-1.2.16+ds1/pollers.php +@@ -397,9 +397,9 @@ function poller_host_duplicate($poller_i + } else { + return db_fetch_cell_prepared('SELECT COUNT(*) + FROM poller +- WHERE dbhost LIKE "' . $host . '%" ++ WHERE dbhost LIKE ? + AND id != ?', +- array($poller_id)); ++ array($host . '%', $poller_id)); + } + } + diff -Nru cacti-1.2.16+ds1/debian/patches/CVE-2023-49086.patch cacti-1.2.16+ds1/debian/patches/CVE-2023-49086.patch --- cacti-1.2.16+ds1/debian/patches/CVE-2023-49086.patch 1970-01-01 00:00:00.000000000 +0000 +++ cacti-1.2.16+ds1/debian/patches/CVE-2023-49086.patch 2024-03-15 09:04:12.000000000 +0000 @@ -0,0 +1,49 @@ +Origin: https://github.com/cacti/cacti/commit/56f9d99e6e5ab434ea18fa344236f41e78f99c59 +Reviewed-by: Sylvain Beucler +Last-Update: 2024-02-20 + +From 56f9d99e6e5ab434ea18fa344236f41e78f99c59 Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Thu, 28 Dec 2023 13:53:57 -0500 +Subject: [PATCH] QA: Fix 2 of 3 - Commits for CVE-2023-49088 and + CVE-2023-48086 + +Missed here https://github.com/Cacti/cacti/security/advisories/GHSA-q7g7-gcf6-wh4x +and here: +https://github.com/Cacti/cacti/security/advisories/GHSA-wc73-r2vw-59pr +--- + data_debug.php | 2 +- + graphs_new.php | 20 ++++++++++++++++++-- + 2 files changed, 19 insertions(+), 3 deletions(-) + +Index: cacti-1.2.16+ds1/graphs_new.php +=================================================================== +--- cacti-1.2.16+ds1.orig/graphs_new.php ++++ cacti-1.2.16+ds1/graphs_new.php +@@ -884,8 +884,24 @@ function graphs() { + form_hidden_box('host_template_id', $host['host_template_id'], '0'); + } + +- if (isset($_SERVER['HTTP_REFERER']) && !substr_count($_SERVER['HTTP_REFERER'], 'graphs_new')) { +- set_request_var('returnto', basename(sanitize_uri($_SERVER['HTTP_REFERER']))); ++ if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != '') { ++ $referer_url = parse_url($_SERVER['HTTP_REFERER']); ++ ++ if ($_SERVER['SERVER_NAME'] != $referer_url['host']) { ++ /* Potential security exploit 1 */ ++ set_request_var('returnto', 'host.php'); ++ } elseif (strpos($_SERVER['HTTP_REFERER'], 'graphs_new') === false) { ++ set_request_var('returnto', basename($_SERVER['HTTP_REFERER'])); ++ } else { ++ set_request_var('returnto', 'host.php'); ++ } ++ } elseif (isset_request_var('returnto') && get_nfilter_request_var('returnto') != '') { ++ $returnto_url = parse_url(get_nfilter_request_var('returnto')); ++ ++ if ($_SERVER['SERVER_NAME'] != $returnto_url['host']) { ++ /* Potential security exploit 2 */ ++ set_request_var('returnto', 'host.php'); ++ } + } + + load_current_session_value('returnto', 'sess_grn_returnto', ''); diff -Nru cacti-1.2.16+ds1/debian/patches/CVE-2023-49088.patch cacti-1.2.16+ds1/debian/patches/CVE-2023-49088.patch --- cacti-1.2.16+ds1/debian/patches/CVE-2023-49088.patch 1970-01-01 00:00:00.000000000 +0000 +++ cacti-1.2.16+ds1/debian/patches/CVE-2023-49088.patch 2024-03-15 09:33:43.000000000 +0000 @@ -0,0 +1,1857 @@ +Origin: https://github.com/cacti/cacti/commit/56f9d99e6e5ab434ea18fa344236f41e78f99c59 +Origin: https://github.com/cacti/cacti/commit/58a980f335980ab57659420053d89d4e721ae3fc +Origin: https://github.com/cacti/cacti/commit/73d9a60e24d6d826e6343b94d833b48c28b68643 +Origin: https://github.com/cacti/cacti/commit/59e39b34f8f1d80b28d38a391d7aa6e7a3302f5b +Reviewed-by: Sylvain Beucler +Last-Update: 2024-03-15 + +Backport note: 56f9d99e6e5ab434ea18fa344236f41e78f99c59 isn't the +actual fix for CVE-2023-49088, but including it for completeness. + +Backport note: embedding purify.js as node-dompurify isn't in bullseye. + +From 56f9d99e6e5ab434ea18fa344236f41e78f99c59 Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Thu, 28 Dec 2023 13:53:57 -0500 +Subject: [PATCH] QA: Fix 2 of 3 - Commits for CVE-2023-49088 and + CVE-2023-48086 + +Missed here https://github.com/Cacti/cacti/security/advisories/GHSA-q7g7-gcf6-wh4x +and here: +https://github.com/Cacti/cacti/security/advisories/GHSA-wc73-r2vw-59pr +--- + data_debug.php | 2 +- + graphs_new.php | 20 ++++++++++++++++++-- + 2 files changed, 19 insertions(+), 3 deletions(-) + +From 58a980f335980ab57659420053d89d4e721ae3fc Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Wed, 20 Dec 2023 16:24:54 -0500 +Subject: [PATCH] QA: Address multiple security issues (#5623) + +--- + include/js/purify.js | 1490 ++++++++++++++++++++++++++++++++++++++++++ + include/layout.js | 5 +- + lib/functions.php | 6 +- + lib/html.php | 1 + + managers.php | 11 +- + utilities.php | 4 +- + 6 files changed, 1509 insertions(+), 8 deletions(-) + create mode 100644 include/js/purify.js + +From 73d9a60e24d6d826e6343b94d833b48c28b68643 Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Wed, 20 Dec 2023 18:32:29 -0500 +Subject: [PATCH] QA: Fix Purify from GitHub (#5625) + +--- + include/js/purify.js | 592 +++++++++++++++++++++++++++++-------------- + 1 file changed, 398 insertions(+), 194 deletions(-) + +From 59e39b34f8f1d80b28d38a391d7aa6e7a3302f5b Mon Sep 17 00:00:00 2001 +From: TheWitness +Date: Thu, 28 Dec 2023 12:58:07 -0500 +Subject: [PATCH] QA: Fix 1 of 3 - Two 1.2.26 release merge issues + +Due to not properly merging of the security solutions, these two errors are present in the 1.2.26 release. +--- + include/layout.js | 2 +- + utilities.php | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +Index: cacti-1.2.16+ds1/data_debug.php +=================================================================== +--- cacti-1.2.16+ds1.orig/data_debug.php ++++ cacti-1.2.16+ds1/data_debug.php +@@ -611,7 +611,7 @@ function debug_view() { + array($id)); + + if (cacti_sizeof($dtd)) { +- $real_path = str_replace('', $config['rra_path'], $dtd['data_source_path']); ++ $real_path = html_escape(str_replace('', $config['rra_path'], $dtd['data_source_path'])); + } else { + $real_path = __('Not Found'); + } +Index: cacti-1.2.16+ds1/include/js/purify.js +=================================================================== +--- /dev/null ++++ cacti-1.2.16+ds1/include/js/purify.js +@@ -0,0 +1,1694 @@ ++/*! @license DOMPurify 3.0.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.6/LICENSE */ ++ ++(function (global, factory) { ++ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : ++ typeof define === 'function' && define.amd ? define(factory) : ++ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.DOMPurify = factory()); ++})(this, (function () { 'use strict'; ++ ++ const { ++ entries, ++ setPrototypeOf, ++ isFrozen, ++ getPrototypeOf, ++ getOwnPropertyDescriptor ++ } = Object; ++ let { ++ freeze, ++ seal, ++ create ++ } = Object; // eslint-disable-line import/no-mutable-exports ++ ++ let { ++ apply, ++ construct ++ } = typeof Reflect !== 'undefined' && Reflect; ++ ++ if (!freeze) { ++ freeze = function freeze(x) { ++ return x; ++ }; ++ } ++ ++ if (!seal) { ++ seal = function seal(x) { ++ return x; ++ }; ++ } ++ ++ if (!apply) { ++ apply = function apply(fun, thisValue, args) { ++ return fun.apply(thisValue, args); ++ }; ++ } ++ ++ if (!construct) { ++ construct = function construct(Func, args) { ++ return new Func(...args); ++ }; ++ } ++ ++ const arrayForEach = unapply(Array.prototype.forEach); ++ const arrayPop = unapply(Array.prototype.pop); ++ const arrayPush = unapply(Array.prototype.push); ++ const stringToLowerCase = unapply(String.prototype.toLowerCase); ++ const stringToString = unapply(String.prototype.toString); ++ const stringMatch = unapply(String.prototype.match); ++ const stringReplace = unapply(String.prototype.replace); ++ const stringIndexOf = unapply(String.prototype.indexOf); ++ const stringTrim = unapply(String.prototype.trim); ++ const regExpTest = unapply(RegExp.prototype.test); ++ const typeErrorCreate = unconstruct(TypeError); ++ /** ++ * Creates a new function that calls the given function with a specified thisArg and arguments. ++ * ++ * @param {Function} func - The function to be wrapped and called. ++ * @returns {Function} A new function that calls the given function with a specified thisArg and arguments. ++ */ ++ ++ function unapply(func) { ++ return function (thisArg) { ++ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { ++ args[_key - 1] = arguments[_key]; ++ } ++ ++ return apply(func, thisArg, args); ++ }; ++ } ++ /** ++ * Creates a new function that constructs an instance of the given constructor function with the provided arguments. ++ * ++ * @param {Function} func - The constructor function to be wrapped and called. ++ * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments. ++ */ ++ ++ ++ function unconstruct(func) { ++ return function () { ++ for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { ++ args[_key2] = arguments[_key2]; ++ } ++ ++ return construct(func, args); ++ }; ++ } ++ /** ++ * Add properties to a lookup table ++ * ++ * @param {Object} set - The set to which elements will be added. ++ * @param {Array} array - The array containing elements to be added to the set. ++ * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set. ++ * @returns {Object} The modified set with added elements. ++ */ ++ ++ ++ function addToSet(set, array) { ++ let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; ++ ++ if (setPrototypeOf) { ++ // Make 'in' and truthy checks like Boolean(set.constructor) ++ // independent of any properties defined on Object.prototype. ++ // Prevent prototype setters from intercepting set as a this value. ++ setPrototypeOf(set, null); ++ } ++ ++ let l = array.length; ++ ++ while (l--) { ++ let element = array[l]; ++ ++ if (typeof element === 'string') { ++ const lcElement = transformCaseFunc(element); ++ ++ if (lcElement !== element) { ++ // Config presets (e.g. tags.js, attrs.js) are immutable. ++ if (!isFrozen(array)) { ++ array[l] = lcElement; ++ } ++ ++ element = lcElement; ++ } ++ } ++ ++ set[element] = true; ++ } ++ ++ return set; ++ } ++ /** ++ * Clean up an array to harden against CSPP ++ * ++ * @param {Array} array - The array to be cleaned. ++ * @returns {Array} The cleaned version of the array ++ */ ++ ++ ++ function cleanArray(array) { ++ for (let index = 0; index < array.length; index++) { ++ if (getOwnPropertyDescriptor(array, index) === undefined) { ++ array[index] = null; ++ } ++ } ++ ++ return array; ++ } ++ /** ++ * Shallow clone an object ++ * ++ * @param {Object} object - The object to be cloned. ++ * @returns {Object} A new object that copies the original. ++ */ ++ ++ ++ function clone(object) { ++ const newObject = create(null); ++ ++ for (const [property, value] of entries(object)) { ++ if (getOwnPropertyDescriptor(object, property) !== undefined) { ++ if (Array.isArray(value)) { ++ newObject[property] = cleanArray(value); ++ } else if (typeof value === 'object' && value.constructor === Object) { ++ newObject[property] = clone(value); ++ } else { ++ newObject[property] = value; ++ } ++ } ++ } ++ ++ return newObject; ++ } ++ /** ++ * This method automatically checks if the prop is function or getter and behaves accordingly. ++ * ++ * @param {Object} object - The object to look up the getter function in its prototype chain. ++ * @param {String} prop - The property name for which to find the getter function. ++ * @returns {Function} The getter function found in the prototype chain or a fallback function. ++ */ ++ ++ ++ function lookupGetter(object, prop) { ++ while (object !== null) { ++ const desc = getOwnPropertyDescriptor(object, prop); ++ ++ if (desc) { ++ if (desc.get) { ++ return unapply(desc.get); ++ } ++ ++ if (typeof desc.value === 'function') { ++ return unapply(desc.value); ++ } ++ } ++ ++ object = getPrototypeOf(object); ++ } ++ ++ function fallbackValue(element) { ++ console.warn('fallback value for', element); ++ return null; ++ } ++ ++ return fallbackValue; ++ } ++ ++ const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); // SVG ++ ++ const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); ++ const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); // List of SVG elements that are disallowed by default. ++ // We still need to know them so that we can do namespace ++ // checks properly in case one wants to add them to ++ // allow-list. ++ ++ const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); ++ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']); // Similarly to SVG, we want to know all MathML elements, ++ // even those that we disallow by default. ++ ++ const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); ++ const text = freeze(['#text']); ++ ++ const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns', 'slot']); ++ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); ++ const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); ++ const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); ++ ++ const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode ++ ++ const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); ++ const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm); ++ const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape ++ ++ const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape ++ ++ const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape ++ ); ++ const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); ++ const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex ++ ); ++ const DOCTYPE_NAME = seal(/^html$/i); ++ ++ var EXPRESSIONS = /*#__PURE__*/Object.freeze({ ++ __proto__: null, ++ MUSTACHE_EXPR: MUSTACHE_EXPR, ++ ERB_EXPR: ERB_EXPR, ++ TMPLIT_EXPR: TMPLIT_EXPR, ++ DATA_ATTR: DATA_ATTR, ++ ARIA_ATTR: ARIA_ATTR, ++ IS_ALLOWED_URI: IS_ALLOWED_URI, ++ IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA, ++ ATTR_WHITESPACE: ATTR_WHITESPACE, ++ DOCTYPE_NAME: DOCTYPE_NAME ++ }); ++ ++ const getGlobal = function getGlobal() { ++ return typeof window === 'undefined' ? null : window; ++ }; ++ /** ++ * Creates a no-op policy for internal use only. ++ * Don't export this function outside this module! ++ * @param {TrustedTypePolicyFactory} trustedTypes The policy factory. ++ * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). ++ * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types ++ * are not supported or creating the policy failed). ++ */ ++ ++ ++ const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { ++ if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { ++ return null; ++ } // Allow the callers to control the unique policy name ++ // by adding a data-tt-policy-suffix to the script element with the DOMPurify. ++ // Policy creation with duplicate names throws in Trusted Types. ++ ++ ++ let suffix = null; ++ const ATTR_NAME = 'data-tt-policy-suffix'; ++ ++ if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { ++ suffix = purifyHostElement.getAttribute(ATTR_NAME); ++ } ++ ++ const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); ++ ++ try { ++ return trustedTypes.createPolicy(policyName, { ++ createHTML(html) { ++ return html; ++ }, ++ ++ createScriptURL(scriptUrl) { ++ return scriptUrl; ++ } ++ ++ }); ++ } catch (_) { ++ // Policy creation failed (most likely another DOMPurify script has ++ // already run). Skip creating the policy, as this will only cause errors ++ // if TT are enforced. ++ console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); ++ return null; ++ } ++ }; ++ ++ function createDOMPurify() { ++ let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); ++ ++ const DOMPurify = root => createDOMPurify(root); ++ /** ++ * Version label, exposed for easier checks ++ * if DOMPurify is up to date or not ++ */ ++ ++ ++ DOMPurify.version = '3.0.6'; ++ /** ++ * Array of elements that DOMPurify removed during sanitation. ++ * Empty if nothing was removed. ++ */ ++ ++ DOMPurify.removed = []; ++ ++ if (!window || !window.document || window.document.nodeType !== 9) { ++ // Not running in a browser, provide a factory function ++ // so that you can pass your own Window ++ DOMPurify.isSupported = false; ++ return DOMPurify; ++ } ++ ++ let { ++ document ++ } = window; ++ const originalDocument = document; ++ const currentScript = originalDocument.currentScript; ++ const { ++ DocumentFragment, ++ HTMLTemplateElement, ++ Node, ++ Element, ++ NodeFilter, ++ NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap, ++ HTMLFormElement, ++ DOMParser, ++ trustedTypes ++ } = window; ++ const ElementPrototype = Element.prototype; ++ const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); ++ const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); ++ const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); ++ const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); // As per issue #47, the web-components registry is inherited by a ++ // new document created via createHTMLDocument. As per the spec ++ // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) ++ // a new empty registry is used when creating a template contents owner ++ // document, so we use that as our parent document to ensure nothing ++ // is inherited. ++ ++ if (typeof HTMLTemplateElement === 'function') { ++ const template = document.createElement('template'); ++ ++ if (template.content && template.content.ownerDocument) { ++ document = template.content.ownerDocument; ++ } ++ } ++ ++ let trustedTypesPolicy; ++ let emptyHTML = ''; ++ const { ++ implementation, ++ createNodeIterator, ++ createDocumentFragment, ++ getElementsByTagName ++ } = document; ++ const { ++ importNode ++ } = originalDocument; ++ let hooks = {}; ++ /** ++ * Expose whether this browser supports running the full DOMPurify. ++ */ ++ ++ DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; ++ const { ++ MUSTACHE_EXPR, ++ ERB_EXPR, ++ TMPLIT_EXPR, ++ DATA_ATTR, ++ ARIA_ATTR, ++ IS_SCRIPT_OR_DATA, ++ ATTR_WHITESPACE ++ } = EXPRESSIONS; ++ let { ++ IS_ALLOWED_URI: IS_ALLOWED_URI$1 ++ } = EXPRESSIONS; ++ /** ++ * We consider the elements and attributes below to be safe. Ideally ++ * don't add any new ones but feel free to remove unwanted ones. ++ */ ++ ++ /* allowed element names */ ++ ++ let ALLOWED_TAGS = null; ++ const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); ++ /* Allowed attribute names */ ++ ++ let ALLOWED_ATTR = null; ++ const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); ++ /* ++ * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements. ++ * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) ++ * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) ++ * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. ++ */ ++ ++ let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { ++ tagNameCheck: { ++ writable: true, ++ configurable: false, ++ enumerable: true, ++ value: null ++ }, ++ attributeNameCheck: { ++ writable: true, ++ configurable: false, ++ enumerable: true, ++ value: null ++ }, ++ allowCustomizedBuiltInElements: { ++ writable: true, ++ configurable: false, ++ enumerable: true, ++ value: false ++ } ++ })); ++ /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ ++ ++ let FORBID_TAGS = null; ++ /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ ++ ++ let FORBID_ATTR = null; ++ /* Decide if ARIA attributes are okay */ ++ ++ let ALLOW_ARIA_ATTR = true; ++ /* Decide if custom data attributes are okay */ ++ ++ let ALLOW_DATA_ATTR = true; ++ /* Decide if unknown protocols are okay */ ++ ++ let ALLOW_UNKNOWN_PROTOCOLS = false; ++ /* Decide if self-closing tags in attributes are allowed. ++ * Usually removed due to a mXSS issue in jQuery 3.0 */ ++ ++ let ALLOW_SELF_CLOSE_IN_ATTR = true; ++ /* Output should be safe for common template engines. ++ * This means, DOMPurify removes data attributes, mustaches and ERB ++ */ ++ ++ let SAFE_FOR_TEMPLATES = false; ++ /* Decide if document with ... should be returned */ ++ ++ let WHOLE_DOCUMENT = false; ++ /* Track whether config is already set on this instance of DOMPurify. */ ++ ++ let SET_CONFIG = false; ++ /* Decide if all elements (e.g. style, script) must be children of ++ * document.body. By default, browsers might move them to document.head */ ++ ++ let FORCE_BODY = false; ++ /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html ++ * string (or a TrustedHTML object if Trusted Types are supported). ++ * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead ++ */ ++ ++ let RETURN_DOM = false; ++ /* Decide if a DOM `DocumentFragment` should be returned, instead of a html ++ * string (or a TrustedHTML object if Trusted Types are supported) */ ++ ++ let RETURN_DOM_FRAGMENT = false; ++ /* Try to return a Trusted Type object instead of a string, return a string in ++ * case Trusted Types are not supported */ ++ ++ let RETURN_TRUSTED_TYPE = false; ++ /* Output should be free from DOM clobbering attacks? ++ * This sanitizes markups named with colliding, clobberable built-in DOM APIs. ++ */ ++ ++ let SANITIZE_DOM = true; ++ /* Achieve full DOM Clobbering protection by isolating the namespace of named ++ * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. ++ * ++ * HTML/DOM spec rules that enable DOM Clobbering: ++ * - Named Access on Window (§7.3.3) ++ * - DOM Tree Accessors (§3.1.5) ++ * - Form Element Parent-Child Relations (§4.10.3) ++ * - Iframe srcdoc / Nested WindowProxies (§4.8.5) ++ * - HTMLCollection (§4.2.10.2) ++ * ++ * Namespace isolation is implemented by prefixing `id` and `name` attributes ++ * with a constant string, i.e., `user-content-` ++ */ ++ ++ let SANITIZE_NAMED_PROPS = false; ++ const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; ++ /* Keep element content when removing element? */ ++ ++ let KEEP_CONTENT = true; ++ /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead ++ * of importing it into a new Document and returning a sanitized copy */ ++ ++ let IN_PLACE = false; ++ /* Allow usage of profiles like html, svg and mathMl */ ++ ++ let USE_PROFILES = {}; ++ /* Tags to ignore content of when KEEP_CONTENT is true */ ++ ++ let FORBID_CONTENTS = null; ++ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); ++ /* Tags that are safe for data: URIs */ ++ ++ let DATA_URI_TAGS = null; ++ const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); ++ /* Attributes safe for values like "javascript:" */ ++ ++ let URI_SAFE_ATTRIBUTES = null; ++ const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); ++ const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; ++ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; ++ const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; ++ /* Document namespace */ ++ ++ let NAMESPACE = HTML_NAMESPACE; ++ let IS_EMPTY_INPUT = false; ++ /* Allowed XHTML+XML namespaces */ ++ ++ let ALLOWED_NAMESPACES = null; ++ const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); ++ /* Parsing of strict XHTML documents */ ++ ++ let PARSER_MEDIA_TYPE = null; ++ const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; ++ const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; ++ let transformCaseFunc = null; ++ /* Keep a reference to config to pass to hooks */ ++ ++ let CONFIG = null; ++ /* Ideally, do not touch anything below this line */ ++ ++ /* ______________________________________________ */ ++ ++ const formElement = document.createElement('form'); ++ ++ const isRegexOrFunction = function isRegexOrFunction(testValue) { ++ return testValue instanceof RegExp || testValue instanceof Function; ++ }; ++ /** ++ * _parseConfig ++ * ++ * @param {Object} cfg optional config literal ++ */ ++ // eslint-disable-next-line complexity ++ ++ ++ const _parseConfig = function _parseConfig() { ++ let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; ++ ++ if (CONFIG && CONFIG === cfg) { ++ return; ++ } ++ /* Shield configuration object from tampering */ ++ ++ ++ if (!cfg || typeof cfg !== 'object') { ++ cfg = {}; ++ } ++ /* Shield configuration object from prototype pollution */ ++ ++ ++ cfg = clone(cfg); ++ PARSER_MEDIA_TYPE = // eslint-disable-next-line unicorn/prefer-includes ++ SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. ++ ++ transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; ++ /* Set configuration parameters */ ++ ++ ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; ++ ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; ++ ALLOWED_NAMESPACES = 'ALLOWED_NAMESPACES' in cfg ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; ++ URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), // eslint-disable-line indent ++ cfg.ADD_URI_SAFE_ATTR, // eslint-disable-line indent ++ transformCaseFunc // eslint-disable-line indent ++ ) // eslint-disable-line indent ++ : DEFAULT_URI_SAFE_ATTRIBUTES; ++ DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), // eslint-disable-line indent ++ cfg.ADD_DATA_URI_TAGS, // eslint-disable-line indent ++ transformCaseFunc // eslint-disable-line indent ++ ) // eslint-disable-line indent ++ : DEFAULT_DATA_URI_TAGS; ++ FORBID_CONTENTS = 'FORBID_CONTENTS' in cfg ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; ++ FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {}; ++ FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {}; ++ USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false; ++ ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true ++ ++ ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true ++ ++ ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false ++ ++ ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true ++ ++ SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false ++ ++ WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false ++ ++ RETURN_DOM = cfg.RETURN_DOM || false; // Default false ++ ++ RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false ++ ++ RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false ++ ++ FORCE_BODY = cfg.FORCE_BODY || false; // Default false ++ ++ SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true ++ ++ SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false ++ ++ KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true ++ ++ IN_PLACE = cfg.IN_PLACE || false; // Default false ++ ++ IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI; ++ NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; ++ CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {}; ++ ++ if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { ++ CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; ++ } ++ ++ if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { ++ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; ++ } ++ ++ if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { ++ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; ++ } ++ ++ if (SAFE_FOR_TEMPLATES) { ++ ALLOW_DATA_ATTR = false; ++ } ++ ++ if (RETURN_DOM_FRAGMENT) { ++ RETURN_DOM = true; ++ } ++ /* Parse profile info */ ++ ++ ++ if (USE_PROFILES) { ++ ALLOWED_TAGS = addToSet({}, text); ++ ALLOWED_ATTR = []; ++ ++ if (USE_PROFILES.html === true) { ++ addToSet(ALLOWED_TAGS, html$1); ++ addToSet(ALLOWED_ATTR, html); ++ } ++ ++ if (USE_PROFILES.svg === true) { ++ addToSet(ALLOWED_TAGS, svg$1); ++ addToSet(ALLOWED_ATTR, svg); ++ addToSet(ALLOWED_ATTR, xml); ++ } ++ ++ if (USE_PROFILES.svgFilters === true) { ++ addToSet(ALLOWED_TAGS, svgFilters); ++ addToSet(ALLOWED_ATTR, svg); ++ addToSet(ALLOWED_ATTR, xml); ++ } ++ ++ if (USE_PROFILES.mathMl === true) { ++ addToSet(ALLOWED_TAGS, mathMl$1); ++ addToSet(ALLOWED_ATTR, mathMl); ++ addToSet(ALLOWED_ATTR, xml); ++ } ++ } ++ /* Merge configuration parameters */ ++ ++ ++ if (cfg.ADD_TAGS) { ++ if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { ++ ALLOWED_TAGS = clone(ALLOWED_TAGS); ++ } ++ ++ addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); ++ } ++ ++ if (cfg.ADD_ATTR) { ++ if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { ++ ALLOWED_ATTR = clone(ALLOWED_ATTR); ++ } ++ ++ addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); ++ } ++ ++ if (cfg.ADD_URI_SAFE_ATTR) { ++ addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); ++ } ++ ++ if (cfg.FORBID_CONTENTS) { ++ if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { ++ FORBID_CONTENTS = clone(FORBID_CONTENTS); ++ } ++ ++ addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); ++ } ++ /* Add #text in case KEEP_CONTENT is set to true */ ++ ++ ++ if (KEEP_CONTENT) { ++ ALLOWED_TAGS['#text'] = true; ++ } ++ /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ ++ ++ ++ if (WHOLE_DOCUMENT) { ++ addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); ++ } ++ /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ ++ ++ ++ if (ALLOWED_TAGS.table) { ++ addToSet(ALLOWED_TAGS, ['tbody']); ++ delete FORBID_TAGS.tbody; ++ } ++ ++ if (cfg.TRUSTED_TYPES_POLICY) { ++ if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { ++ throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); ++ } ++ ++ if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { ++ throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); ++ } // Overwrite existing TrustedTypes policy. ++ ++ ++ trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; // Sign local variables required by `sanitize`. ++ ++ emptyHTML = trustedTypesPolicy.createHTML(''); ++ } else { ++ // Uninitialized policy, attempt to initialize the internal dompurify policy. ++ if (trustedTypesPolicy === undefined) { ++ trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); ++ } // If creating the internal policy succeeded sign internal variables. ++ ++ ++ if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') { ++ emptyHTML = trustedTypesPolicy.createHTML(''); ++ } ++ } // Prevent further manipulation of configuration. ++ // Not available in IE8, Safari 5, etc. ++ ++ ++ if (freeze) { ++ freeze(cfg); ++ } ++ ++ CONFIG = cfg; ++ }; ++ ++ const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); ++ const HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']); // Certain elements are allowed in both SVG and HTML ++ // namespace. We need to specify them explicitly ++ // so that they don't get erroneously deleted from ++ // HTML namespace. ++ ++ const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); ++ /* Keep track of all possible SVG and MathML tags ++ * so that we can perform the namespace checks ++ * correctly. */ ++ ++ const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); ++ const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); ++ /** ++ * @param {Element} element a DOM element whose namespace is being checked ++ * @returns {boolean} Return false if the element has a ++ * namespace that a spec-compliant parser would never ++ * return. Return true otherwise. ++ */ ++ ++ const _checkValidNamespace = function _checkValidNamespace(element) { ++ let parent = getParentNode(element); // In JSDOM, if we're inside shadow DOM, then parentNode ++ // can be null. We just simulate parent in this case. ++ ++ if (!parent || !parent.tagName) { ++ parent = { ++ namespaceURI: NAMESPACE, ++ tagName: 'template' ++ }; ++ } ++ ++ const tagName = stringToLowerCase(element.tagName); ++ const parentTagName = stringToLowerCase(parent.tagName); ++ ++ if (!ALLOWED_NAMESPACES[element.namespaceURI]) { ++ return false; ++ } ++ ++ if (element.namespaceURI === SVG_NAMESPACE) { ++ // The only way to switch from HTML namespace to SVG ++ // is via . If it happens via any other tag, then ++ // it should be killed. ++ if (parent.namespaceURI === HTML_NAMESPACE) { ++ return tagName === 'svg'; ++ } // The only way to switch from MathML to SVG is via` ++ // svg if parent is either or MathML ++ // text integration points. ++ ++ ++ if (parent.namespaceURI === MATHML_NAMESPACE) { ++ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); ++ } // We only allow elements that are defined in SVG ++ // spec. All others are disallowed in SVG namespace. ++ ++ ++ return Boolean(ALL_SVG_TAGS[tagName]); ++ } ++ ++ if (element.namespaceURI === MATHML_NAMESPACE) { ++ // The only way to switch from HTML namespace to MathML ++ // is via . If it happens via any other tag, then ++ // it should be killed. ++ if (parent.namespaceURI === HTML_NAMESPACE) { ++ return tagName === 'math'; ++ } // The only way to switch from SVG to MathML is via ++ // and HTML integration points ++ ++ ++ if (parent.namespaceURI === SVG_NAMESPACE) { ++ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; ++ } // We only allow elements that are defined in MathML ++ // spec. All others are disallowed in MathML namespace. ++ ++ ++ return Boolean(ALL_MATHML_TAGS[tagName]); ++ } ++ ++ if (element.namespaceURI === HTML_NAMESPACE) { ++ // The only way to switch from SVG to HTML is via ++ // HTML integration points, and from MathML to HTML ++ // is via MathML text integration points ++ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { ++ return false; ++ } ++ ++ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { ++ return false; ++ } // We disallow tags that are specific for MathML ++ // or SVG and should never appear in HTML namespace ++ ++ ++ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); ++ } // For XHTML and XML documents that support custom namespaces ++ ++ ++ if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { ++ return true; ++ } // The code should never reach this place (this means ++ // that the element somehow got namespace that is not ++ // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). ++ // Return false just in case. ++ ++ ++ return false; ++ }; ++ /** ++ * _forceRemove ++ * ++ * @param {Node} node a DOM node ++ */ ++ ++ ++ const _forceRemove = function _forceRemove(node) { ++ arrayPush(DOMPurify.removed, { ++ element: node ++ }); ++ ++ try { ++ // eslint-disable-next-line unicorn/prefer-dom-node-remove ++ node.parentNode.removeChild(node); ++ } catch (_) { ++ node.remove(); ++ } ++ }; ++ /** ++ * _removeAttribute ++ * ++ * @param {String} name an Attribute name ++ * @param {Node} node a DOM node ++ */ ++ ++ ++ const _removeAttribute = function _removeAttribute(name, node) { ++ try { ++ arrayPush(DOMPurify.removed, { ++ attribute: node.getAttributeNode(name), ++ from: node ++ }); ++ } catch (_) { ++ arrayPush(DOMPurify.removed, { ++ attribute: null, ++ from: node ++ }); ++ } ++ ++ node.removeAttribute(name); // We void attribute values for unremovable "is"" attributes ++ ++ if (name === 'is' && !ALLOWED_ATTR[name]) { ++ if (RETURN_DOM || RETURN_DOM_FRAGMENT) { ++ try { ++ _forceRemove(node); ++ } catch (_) {} ++ } else { ++ try { ++ node.setAttribute(name, ''); ++ } catch (_) {} ++ } ++ } ++ }; ++ /** ++ * _initDocument ++ * ++ * @param {String} dirty a string of dirty markup ++ * @return {Document} a DOM, filled with the dirty markup ++ */ ++ ++ ++ const _initDocument = function _initDocument(dirty) { ++ /* Create a HTML document */ ++ let doc = null; ++ let leadingWhitespace = null; ++ ++ if (FORCE_BODY) { ++ dirty = '' + dirty; ++ } else { ++ /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ ++ const matches = stringMatch(dirty, /^[\r\n\t ]+/); ++ leadingWhitespace = matches && matches[0]; ++ } ++ ++ if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { ++ // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) ++ dirty = '' + dirty + ''; ++ } ++ ++ const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; ++ /* ++ * Use the DOMParser API by default, fallback later if needs be ++ * DOMParser not work for svg when has multiple root element. ++ */ ++ ++ if (NAMESPACE === HTML_NAMESPACE) { ++ try { ++ doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); ++ } catch (_) {} ++ } ++ /* Use createHTMLDocument in case DOMParser is not available */ ++ ++ ++ if (!doc || !doc.documentElement) { ++ doc = implementation.createDocument(NAMESPACE, 'template', null); ++ ++ try { ++ doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; ++ } catch (_) {// Syntax error if dirtyPayload is invalid xml ++ } ++ } ++ ++ const body = doc.body || doc.documentElement; ++ ++ if (dirty && leadingWhitespace) { ++ body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); ++ } ++ /* Work on whole document or just its body */ ++ ++ ++ if (NAMESPACE === HTML_NAMESPACE) { ++ return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; ++ } ++ ++ return WHOLE_DOCUMENT ? doc.documentElement : body; ++ }; ++ /** ++ * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. ++ * ++ * @param {Node} root The root element or node to start traversing on. ++ * @return {NodeIterator} The created NodeIterator ++ */ ++ ++ ++ const _createNodeIterator = function _createNodeIterator(root) { ++ return createNodeIterator.call(root.ownerDocument || root, root, // eslint-disable-next-line no-bitwise ++ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, null); ++ }; ++ /** ++ * _isClobbered ++ * ++ * @param {Node} elm element to check for clobbering attacks ++ * @return {Boolean} true if clobbered, false if safe ++ */ ++ ++ ++ const _isClobbered = function _isClobbered(elm) { ++ return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function'); ++ }; ++ /** ++ * Checks whether the given object is a DOM node. ++ * ++ * @param {Node} object object to check whether it's a DOM node ++ * @return {Boolean} true is object is a DOM node ++ */ ++ ++ ++ const _isNode = function _isNode(object) { ++ return typeof Node === 'function' && object instanceof Node; ++ }; ++ /** ++ * _executeHook ++ * Execute user configurable hooks ++ * ++ * @param {String} entryPoint Name of the hook's entry point ++ * @param {Node} currentNode node to work on with the hook ++ * @param {Object} data additional hook parameters ++ */ ++ ++ ++ const _executeHook = function _executeHook(entryPoint, currentNode, data) { ++ if (!hooks[entryPoint]) { ++ return; ++ } ++ ++ arrayForEach(hooks[entryPoint], hook => { ++ hook.call(DOMPurify, currentNode, data, CONFIG); ++ }); ++ }; ++ /** ++ * _sanitizeElements ++ * ++ * @protect nodeName ++ * @protect textContent ++ * @protect removeChild ++ * ++ * @param {Node} currentNode to check for permission to exist ++ * @return {Boolean} true if node was killed, false if left alive ++ */ ++ ++ ++ const _sanitizeElements = function _sanitizeElements(currentNode) { ++ let content = null; ++ /* Execute a hook if present */ ++ ++ _executeHook('beforeSanitizeElements', currentNode, null); ++ /* Check if element is clobbered or can clobber */ ++ ++ ++ if (_isClobbered(currentNode)) { ++ _forceRemove(currentNode); ++ ++ return true; ++ } ++ /* Now let's check the element's type and name */ ++ ++ ++ const tagName = transformCaseFunc(currentNode.nodeName); ++ /* Execute a hook if present */ ++ ++ _executeHook('uponSanitizeElement', currentNode, { ++ tagName, ++ allowedTags: ALLOWED_TAGS ++ }); ++ /* Detect mXSS attempts abusing namespace confusion */ ++ ++ ++ if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { ++ _forceRemove(currentNode); ++ ++ return true; ++ } ++ /* Remove element if anything forbids its presence */ ++ ++ ++ if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { ++ /* Check if we have a custom element to handle */ ++ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { ++ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { ++ return false; ++ } ++ ++ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { ++ return false; ++ } ++ } ++ /* Keep content except for bad-listed elements */ ++ ++ ++ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { ++ const parentNode = getParentNode(currentNode) || currentNode.parentNode; ++ const childNodes = getChildNodes(currentNode) || currentNode.childNodes; ++ ++ if (childNodes && parentNode) { ++ const childCount = childNodes.length; ++ ++ for (let i = childCount - 1; i >= 0; --i) { ++ parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode)); ++ } ++ } ++ } ++ ++ _forceRemove(currentNode); ++ ++ return true; ++ } ++ /* Check whether element has a valid namespace */ ++ ++ ++ if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { ++ _forceRemove(currentNode); ++ ++ return true; ++ } ++ /* Make sure that older browsers don't get fallback-tag mXSS */ ++ ++ ++ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { ++ _forceRemove(currentNode); ++ ++ return true; ++ } ++ /* Sanitize element content to be template-safe */ ++ ++ ++ if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) { ++ /* Get the element's text content */ ++ content = currentNode.textContent; ++ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { ++ content = stringReplace(content, expr, ' '); ++ }); ++ ++ if (currentNode.textContent !== content) { ++ arrayPush(DOMPurify.removed, { ++ element: currentNode.cloneNode() ++ }); ++ currentNode.textContent = content; ++ } ++ } ++ /* Execute a hook if present */ ++ ++ ++ _executeHook('afterSanitizeElements', currentNode, null); ++ ++ return false; ++ }; ++ /** ++ * _isValidAttribute ++ * ++ * @param {string} lcTag Lowercase tag name of containing element. ++ * @param {string} lcName Lowercase attribute name. ++ * @param {string} value Attribute value. ++ * @return {Boolean} Returns true if `value` is valid, otherwise false. ++ */ ++ // eslint-disable-next-line complexity ++ ++ ++ const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { ++ /* Make sure attribute cannot clobber */ ++ if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { ++ return false; ++ } ++ /* Allow valid data-* attributes: At least one character after "-" ++ (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) ++ XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) ++ We don't need to check the value; it's always URI safe. */ ++ ++ ++ if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { ++ if ( // First condition does a very basic check if a) it's basically a valid custom element tagname AND ++ // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck ++ // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck ++ _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) || // Alternative, second condition checks if it's an `is`-attribute, AND ++ // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck ++ lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else { ++ return false; ++ } ++ /* Check value is safe. First, is attr inert? If so, is safe */ ++ ++ } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) { ++ return false; ++ } else ; ++ ++ return true; ++ }; ++ /** ++ * _isBasicCustomElement ++ * checks if at least one dash is included in tagName, and it's not the first char ++ * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name ++ * ++ * @param {string} tagName name of the tag of the node to sanitize ++ * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false. ++ */ ++ ++ ++ const _isBasicCustomElement = function _isBasicCustomElement(tagName) { ++ return tagName.indexOf('-') > 0; ++ }; ++ /** ++ * _sanitizeAttributes ++ * ++ * @protect attributes ++ * @protect nodeName ++ * @protect removeAttribute ++ * @protect setAttribute ++ * ++ * @param {Node} currentNode to sanitize ++ */ ++ ++ ++ const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { ++ /* Execute a hook if present */ ++ _executeHook('beforeSanitizeAttributes', currentNode, null); ++ ++ const { ++ attributes ++ } = currentNode; ++ /* Check if we have attributes; if not we might have a text node */ ++ ++ if (!attributes) { ++ return; ++ } ++ ++ const hookEvent = { ++ attrName: '', ++ attrValue: '', ++ keepAttr: true, ++ allowedAttributes: ALLOWED_ATTR ++ }; ++ let l = attributes.length; ++ /* Go backwards over all attributes; safely remove bad ones */ ++ ++ while (l--) { ++ const attr = attributes[l]; ++ const { ++ name, ++ namespaceURI, ++ value: attrValue ++ } = attr; ++ const lcName = transformCaseFunc(name); ++ let value = name === 'value' ? attrValue : stringTrim(attrValue); ++ /* Execute a hook if present */ ++ ++ hookEvent.attrName = lcName; ++ hookEvent.attrValue = value; ++ hookEvent.keepAttr = true; ++ hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set ++ ++ _executeHook('uponSanitizeAttribute', currentNode, hookEvent); ++ ++ value = hookEvent.attrValue; ++ /* Did the hooks approve of the attribute? */ ++ ++ if (hookEvent.forceKeepAttr) { ++ continue; ++ } ++ /* Remove attribute */ ++ ++ ++ _removeAttribute(name, currentNode); ++ /* Did the hooks approve of the attribute? */ ++ ++ ++ if (!hookEvent.keepAttr) { ++ continue; ++ } ++ /* Work around a security issue in jQuery 3.0 */ ++ ++ ++ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { ++ _removeAttribute(name, currentNode); ++ ++ continue; ++ } ++ /* Sanitize attribute content to be template-safe */ ++ ++ ++ if (SAFE_FOR_TEMPLATES) { ++ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { ++ value = stringReplace(value, expr, ' '); ++ }); ++ } ++ /* Is `value` valid for this attribute? */ ++ ++ ++ const lcTag = transformCaseFunc(currentNode.nodeName); ++ ++ if (!_isValidAttribute(lcTag, lcName, value)) { ++ continue; ++ } ++ /* Full DOM Clobbering protection via namespace isolation, ++ * Prefix id and name attributes with `user-content-` ++ */ ++ ++ ++ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) { ++ // Remove the attribute with this value ++ _removeAttribute(name, currentNode); // Prefix the value and later re-create the attribute with the sanitized value ++ ++ ++ value = SANITIZE_NAMED_PROPS_PREFIX + value; ++ } ++ /* Handle attributes that require Trusted Types */ ++ ++ ++ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') { ++ if (namespaceURI) ; else { ++ switch (trustedTypes.getAttributeType(lcTag, lcName)) { ++ case 'TrustedHTML': ++ { ++ value = trustedTypesPolicy.createHTML(value); ++ break; ++ } ++ ++ case 'TrustedScriptURL': ++ { ++ value = trustedTypesPolicy.createScriptURL(value); ++ break; ++ } ++ } ++ } ++ } ++ /* Handle invalid data-* attribute set by try-catching it */ ++ ++ ++ try { ++ if (namespaceURI) { ++ currentNode.setAttributeNS(namespaceURI, name, value); ++ } else { ++ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ ++ currentNode.setAttribute(name, value); ++ } ++ ++ arrayPop(DOMPurify.removed); ++ } catch (_) {} ++ } ++ /* Execute a hook if present */ ++ ++ ++ _executeHook('afterSanitizeAttributes', currentNode, null); ++ }; ++ /** ++ * _sanitizeShadowDOM ++ * ++ * @param {DocumentFragment} fragment to iterate over recursively ++ */ ++ ++ ++ const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) { ++ let shadowNode = null; ++ ++ const shadowIterator = _createNodeIterator(fragment); ++ /* Execute a hook if present */ ++ ++ ++ _executeHook('beforeSanitizeShadowDOM', fragment, null); ++ ++ while (shadowNode = shadowIterator.nextNode()) { ++ /* Execute a hook if present */ ++ _executeHook('uponSanitizeShadowNode', shadowNode, null); ++ /* Sanitize tags and elements */ ++ ++ ++ if (_sanitizeElements(shadowNode)) { ++ continue; ++ } ++ /* Deep shadow DOM detected */ ++ ++ ++ if (shadowNode.content instanceof DocumentFragment) { ++ _sanitizeShadowDOM(shadowNode.content); ++ } ++ /* Check attributes, sanitize if necessary */ ++ ++ ++ _sanitizeAttributes(shadowNode); ++ } ++ /* Execute a hook if present */ ++ ++ ++ _executeHook('afterSanitizeShadowDOM', fragment, null); ++ }; ++ /** ++ * Sanitize ++ * Public method providing core sanitation functionality ++ * ++ * @param {String|Node} dirty string or DOM node ++ * @param {Object} cfg object ++ */ ++ // eslint-disable-next-line complexity ++ ++ ++ DOMPurify.sanitize = function (dirty) { ++ let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; ++ let body = null; ++ let importedNode = null; ++ let currentNode = null; ++ let returnNode = null; ++ /* Make sure we have a string to sanitize. ++ DO NOT return early, as this will return the wrong type if ++ the user has requested a DOM object rather than a string */ ++ ++ IS_EMPTY_INPUT = !dirty; ++ ++ if (IS_EMPTY_INPUT) { ++ dirty = ''; ++ } ++ /* Stringify, in case dirty is an object */ ++ ++ ++ if (typeof dirty !== 'string' && !_isNode(dirty)) { ++ if (typeof dirty.toString === 'function') { ++ dirty = dirty.toString(); ++ ++ if (typeof dirty !== 'string') { ++ throw typeErrorCreate('dirty is not a string, aborting'); ++ } ++ } else { ++ throw typeErrorCreate('toString is not a function'); ++ } ++ } ++ /* Return dirty HTML if DOMPurify cannot run */ ++ ++ ++ if (!DOMPurify.isSupported) { ++ return dirty; ++ } ++ /* Assign config vars */ ++ ++ ++ if (!SET_CONFIG) { ++ _parseConfig(cfg); ++ } ++ /* Clean up removed elements */ ++ ++ ++ DOMPurify.removed = []; ++ /* Check if dirty is correctly typed for IN_PLACE */ ++ ++ if (typeof dirty === 'string') { ++ IN_PLACE = false; ++ } ++ ++ if (IN_PLACE) { ++ /* Do some early pre-sanitization to avoid unsafe root nodes */ ++ if (dirty.nodeName) { ++ const tagName = transformCaseFunc(dirty.nodeName); ++ ++ if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { ++ throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); ++ } ++ } ++ } else if (dirty instanceof Node) { ++ /* If dirty is a DOM element, append to an empty document to avoid ++ elements being stripped by the parser */ ++ body = _initDocument(''); ++ importedNode = body.ownerDocument.importNode(dirty, true); ++ ++ if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') { ++ /* Node is already a body, use as is */ ++ body = importedNode; ++ } else if (importedNode.nodeName === 'HTML') { ++ body = importedNode; ++ } else { ++ // eslint-disable-next-line unicorn/prefer-dom-node-append ++ body.appendChild(importedNode); ++ } ++ } else { ++ /* Exit directly if we have nothing to do */ ++ if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && // eslint-disable-next-line unicorn/prefer-includes ++ dirty.indexOf('<') === -1) { ++ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; ++ } ++ /* Initialize the document to work on */ ++ ++ ++ body = _initDocument(dirty); ++ /* Check we have a DOM node from the data */ ++ ++ if (!body) { ++ return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; ++ } ++ } ++ /* Remove first element node (ours) if FORCE_BODY is set */ ++ ++ ++ if (body && FORCE_BODY) { ++ _forceRemove(body.firstChild); ++ } ++ /* Get node iterator */ ++ ++ ++ const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body); ++ /* Now start iterating over the created document */ ++ ++ ++ while (currentNode = nodeIterator.nextNode()) { ++ /* Sanitize tags and elements */ ++ if (_sanitizeElements(currentNode)) { ++ continue; ++ } ++ /* Shadow DOM detected, sanitize it */ ++ ++ ++ if (currentNode.content instanceof DocumentFragment) { ++ _sanitizeShadowDOM(currentNode.content); ++ } ++ /* Check attributes, sanitize if necessary */ ++ ++ ++ _sanitizeAttributes(currentNode); ++ } ++ /* If we sanitized `dirty` in-place, return it. */ ++ ++ ++ if (IN_PLACE) { ++ return dirty; ++ } ++ /* Return sanitized string or DOM */ ++ ++ ++ if (RETURN_DOM) { ++ if (RETURN_DOM_FRAGMENT) { ++ returnNode = createDocumentFragment.call(body.ownerDocument); ++ ++ while (body.firstChild) { ++ // eslint-disable-next-line unicorn/prefer-dom-node-append ++ returnNode.appendChild(body.firstChild); ++ } ++ } else { ++ returnNode = body; ++ } ++ ++ if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { ++ /* ++ AdoptNode() is not used because internal state is not reset ++ (e.g. the past names map of a HTMLFormElement), this is safe ++ in theory but we would rather not risk another attack vector. ++ The state that is cloned by importNode() is explicitly defined ++ by the specs. ++ */ ++ returnNode = importNode.call(originalDocument, returnNode, true); ++ } ++ ++ return returnNode; ++ } ++ ++ let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; ++ /* Serialize doctype if allowed */ ++ ++ if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { ++ serializedHTML = '\n' + serializedHTML; ++ } ++ /* Sanitize final string template-safe */ ++ ++ ++ if (SAFE_FOR_TEMPLATES) { ++ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { ++ serializedHTML = stringReplace(serializedHTML, expr, ' '); ++ }); ++ } ++ ++ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; ++ }; ++ /** ++ * Public method to set the configuration once ++ * setConfig ++ * ++ * @param {Object} cfg configuration object ++ */ ++ ++ ++ DOMPurify.setConfig = function () { ++ let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; ++ ++ _parseConfig(cfg); ++ ++ SET_CONFIG = true; ++ }; ++ /** ++ * Public method to remove the configuration ++ * clearConfig ++ * ++ */ ++ ++ ++ DOMPurify.clearConfig = function () { ++ CONFIG = null; ++ SET_CONFIG = false; ++ }; ++ /** ++ * Public method to check if an attribute value is valid. ++ * Uses last set config, if any. Otherwise, uses config defaults. ++ * isValidAttribute ++ * ++ * @param {String} tag Tag name of containing element. ++ * @param {String} attr Attribute name. ++ * @param {String} value Attribute value. ++ * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false. ++ */ ++ ++ ++ DOMPurify.isValidAttribute = function (tag, attr, value) { ++ /* Initialize shared config vars if necessary. */ ++ if (!CONFIG) { ++ _parseConfig({}); ++ } ++ ++ const lcTag = transformCaseFunc(tag); ++ const lcName = transformCaseFunc(attr); ++ return _isValidAttribute(lcTag, lcName, value); ++ }; ++ /** ++ * AddHook ++ * Public method to add DOMPurify hooks ++ * ++ * @param {String} entryPoint entry point for the hook to add ++ * @param {Function} hookFunction function to execute ++ */ ++ ++ ++ DOMPurify.addHook = function (entryPoint, hookFunction) { ++ if (typeof hookFunction !== 'function') { ++ return; ++ } ++ ++ hooks[entryPoint] = hooks[entryPoint] || []; ++ arrayPush(hooks[entryPoint], hookFunction); ++ }; ++ /** ++ * RemoveHook ++ * Public method to remove a DOMPurify hook at a given entryPoint ++ * (pops it from the stack of hooks if more are present) ++ * ++ * @param {String} entryPoint entry point for the hook to remove ++ * @return {Function} removed(popped) hook ++ */ ++ ++ ++ DOMPurify.removeHook = function (entryPoint) { ++ if (hooks[entryPoint]) { ++ return arrayPop(hooks[entryPoint]); ++ } ++ }; ++ /** ++ * RemoveHooks ++ * Public method to remove all DOMPurify hooks at a given entryPoint ++ * ++ * @param {String} entryPoint entry point for the hooks to remove ++ */ ++ ++ ++ DOMPurify.removeHooks = function (entryPoint) { ++ if (hooks[entryPoint]) { ++ hooks[entryPoint] = []; ++ } ++ }; ++ /** ++ * RemoveAllHooks ++ * Public method to remove all DOMPurify hooks ++ */ ++ ++ ++ DOMPurify.removeAllHooks = function () { ++ hooks = {}; ++ }; ++ ++ return DOMPurify; ++ } ++ ++ var purify = createDOMPurify(); ++ ++ return purify; ++ ++})); ++//# sourceMappingURL=purify.js.map +Index: cacti-1.2.16+ds1/include/layout.js +=================================================================== +--- cacti-1.2.16+ds1.orig/include/layout.js ++++ cacti-1.2.16+ds1/include/layout.js +@@ -742,10 +742,11 @@ function applySkin() { + var element = $(this); + + if (element.is('div')) { +- var text = $(this).find('span').html(); ++ var text = DOMPurify.sanitize($(this).find('span').html()); + } else if (element.is('span') || element.is('a')) { +- var text = $(this).prop('title'); ++ var text = DOMPurify.sanitize($(this).prop('title')); + } ++ + return text; + } + }); +Index: cacti-1.2.16+ds1/lib/html.php +=================================================================== +--- cacti-1.2.16+ds1.orig/lib/html.php ++++ cacti-1.2.16+ds1/lib/html.php +@@ -2514,6 +2514,7 @@ function html_common_header($title, $sel + print get_md5_include_js('include/js/c3.js', true); + print get_md5_include_js('include/layout.js'); + print get_md5_include_js('include/js/pace.js'); ++ print get_md5_include_js('include/js/purify.js'); + print get_md5_include_js('include/realtime.js', true); + print get_md5_include_js('include/themes/' . $selectedTheme .'/main.js'); + +Index: cacti-1.2.16+ds1/managers.php +=================================================================== +--- cacti-1.2.16+ds1.orig/managers.php ++++ cacti-1.2.16+ds1/managers.php +@@ -362,11 +362,13 @@ function manager_edit() { + + ?> + + $id) { ++ input_validate_input_number($id); ++ } ++ + if (get_nfilter_request_var('drp_action') == '1') { // delete + db_execute('DELETE FROM snmpagent_managers WHERE id IN (' . implode(',' ,$selected_items) . ')'); + db_execute('DELETE FROM snmpagent_managers_notifications WHERE manager_id IN (' . implode(',' ,$selected_items) . ')'); +Index: cacti-1.2.16+ds1/utilities.php +=================================================================== +--- cacti-1.2.16+ds1.orig/utilities.php ++++ cacti-1.2.16+ds1/utilities.php +@@ -2644,13 +2644,15 @@ function snmpagent_utilities_run_cache() + + ?> + +