Files
cacti/package_repos.php
T
TheWitness 41e6d9e033 security: consolidate verified GHSA fixes for develop (batch 4) (#8145)
* security: consolidate verified GHSA fixes for develop (batch 4)

Develop counterparts of the 1.2.x batch-4 fixes in #8144, re-verified against
current develop HEAD.

- GHSA-4rmr-wvjq-qxc2: include/auth.php Basic Auth and guest branches fall
  through to the per-page realm check instead of returning early.
- GHSA-j67j-wpm4-9g3x: the pre-dispatch update_policy branch in user_admin.php
  and user_group_admin.php requires a CSRF-token POST. The denylist -> allowlist
  redesign is tracked for 1.3 in #8143.
- GHSA-2p3x-f5jm-rg6p: the View-Graphs -> poller RCE chain is broken by the
  realm check (4rmr), the update_policy guard (j67j) and shell-free webhits (x5pw).
- GHSA-x5pw-cg7j-mj4r: scripts/webhits.pl runs wc through a shell-free list-form open.
- GHSA-9737-rqh3-h7fh: names interpolated into raise_message() now use __esc()
  across the UI; URL- and HTML-bearing messages are excluded.
- GHSA-9x9h-2577-9w86: the colordropdown input is built as a node and the label
  is no longer round-tripped through parseHTML (the list item already used
  createTextNode on develop).
- GHSA-h96j-8xhc-2p3w: remote_agent.php authorizes on $_SERVER['REMOTE_ADDR'],
  not get_client_addr(), which honours client-settable proxy headers.

Regression guards under tests/Unit/Security/{Auth,Csrf,Xss,ShellInjection}.
php -l, check-changelog.py and the sink/architectural baselines all pass.

* chore: reference PR #8145 on the batch-4 1.3.0-dev CHANGELOG entries

* security: resolve develop merge - keep id-only xmlerror unescaped (matches #8141 h745 test)

* security(GHSA-j67j): use REQUEST_METHOD POST guard instead of raw \ (hardening-pattern-guard)

* security(GHSA-9737): fix review findings - drop double-encoding in __esc, escape missed name sinks, correct plugins format-arg count
2026-10-02 13:10:42 -04:00

535 lines
18 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
require('./include/auth.php');
require_once(CACTI_PATH_LIBRARY . '/PackageListFilter.php');
require_once(CACTI_PATH_LIBRARY . '/poller.php');
require_once(CACTI_PATH_LIBRARY . '/utility.php');
require_once(CACTI_PATH_LIBRARY . '/CactiValidator.php');
use Symfony\Component\Validator\Constraints as Assert;
$actions = [
1 => __('Delete'),
2 => __('Disable'),
3 => __('Enable'),
4 => __('Default')
];
$types = [
0 => __('GitHub Based'),
1 => __('File Based'),
2 => __('Direct URL')
];
// set default action
set_default_action();
switch (grv('action')) {
case 'save':
form_save();
break;
case 'actions':
form_actions();
break;
case 'edit':
top_header();
repo_edit();
bottom_footer();
break;
default:
top_header();
repos();
bottom_footer();
break;
}
function form_save() : void {
global $registered_cacti_names;
if (isrv('save_component_repo')) {
// ================= input validation =================
$save['id'] = CactiValidator::validateInput(gnrv('id'), 'id', [new Assert\NotBlank(), new Assert\Regex('/^[0-9]+$/')], 3);
$save['repo_type'] = CactiValidator::validateInput(gnrv('repo_type'), 'repo_type', [new Assert\NotBlank(), new Assert\Regex('/^[0-2]$/')], 3);
$save['name'] = CactiValidator::validateInput(gnrv('name'), 'name', [new Assert\NotBlank(), new Assert\Length(max: 32)], 3);
$save['repo_location'] = CactiValidator::validateInput(gnrv('repo_location'), 'repo_location', [new Assert\NotBlank(), new Assert\Length(max: 128)], 3);
$save['repo_branch'] = CactiValidator::validateInput(gnrv('repo_branch'), 'repo_branch', [new Assert\Length(max: 128)], 3);
$save['repo_api_key'] = CactiValidator::validateInput(gnrv('repo_api_key'), 'repo_api_key', [new Assert\Length(max: 128)], 3);
$save['enabled'] = (isrv('enabled') ? 'on' : '');
$save['default'] = (isrv('default') ? 'on' : '');
// ====================================================
if (!is_error_message()) {
$id = sql_save($save, 'package_repositories', 'id');
if ($id) {
if ($save['repo_type'] == 0) {
$repoloc = str_replace('github.com', 'raw.githubusercontent.com', $save['repo_location']);
$file = $repoloc . '/' . $save['repo_branch'] . '/package.manifest';
if ($save['repo_api_key'] != '') {
$options = [
'https' => [
'method' => 'GET',
'header' => 'Authorization: Bearer ' . $save['repo_api_key']
]
];
$context = stream_context_create($options);
} else {
$options = [
'https' => [
'method' => 'GET',
]
];
$context = stream_context_create($options);
}
$data = file_get_contents($file, false, $context);
if ($data != '') {
raise_message('repo_exists', __esc('The Repo \'%s\' is Reachable on GitHub.', $save['name']), MESSAGE_LEVEL_INFO);
} else {
raise_message('repo_missing', __esc('The Repo \'%s\' is NOT Reachable on GitHub or the package.manifest file is missing or it could be an invalid branch. Valid Package Locations are normally: https://github.com/Author/RepoName/.', $save['name']), MESSAGE_LEVEL_WARN);
}
} elseif ($save['repo_type'] == 2) {
$file = $save['repo_location'] . '/package.manifest';
$context = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
];
$data = file_get_contents($file, false, stream_context_create($context));
if ($data != '') {
raise_message('repo_exists', __esc('The Repo \'%s\' is Reachable at the URL Location.', $save['name']), MESSAGE_LEVEL_INFO);
} else {
raise_message('repo_missing', __esc('The Repo \'%s\' is NOT Reachable at the URL Location or the package.manifest file is missing.', $save['name']), MESSAGE_LEVEL_WARN);
}
} else {
$file = $save['repo_location'] . '/package.manifest';
if (file_exists($file)) {
raise_message('repo_exists', __esc('The Repo \'%s\' is Reachable on the Local Cacti Server.', $save['name']), MESSAGE_LEVEL_INFO);
} else {
raise_message('repo_missing', __esc('The Repo \'%s\' is NOT Reachable on the Local Cacti Server or the package.manifest file is missing.', $save['name']), MESSAGE_LEVEL_WARN);
}
}
raise_message(1);
} else {
raise_message(2);
}
}
}
header('Location: package_repos.php?header=false&action=edit&id=' . (empty($id) ? gnrv('id') : $id));
}
function form_actions() : void {
global $actions;
// if we are to save this form, instead of display it
if (isrv('selected_items')) {
$selected_items = PackageListFilter::decodeSelectedIds(gnrv('selected_items'));
if ($selected_items != false) {
if (gnrv('drp_action') == '1') { // delete
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
repo_remove($selected_items[$i]);
}
} elseif (gnrv('drp_action') == '2') { // disable
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
repo_disable($selected_items[$i]);
}
} elseif (gnrv('drp_action') == '3') { // enable
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
repo_enable($selected_items[$i]);
}
} elseif (gnrv('drp_action') == '4') { // default
if (cacti_sizeof($selected_items) > 1) {
// error message
} else {
for ($i = 0; ($i < cacti_count($selected_items)); $i++) {
repo_default($selected_items[$i]);
}
}
}
}
header('Location: package_repos.php?header=false');
exit;
}
// setup some variables
$p_list = '';
$p_array = [];
foreach (PackageListFilter::selectedIdsFromPost() as $id) {
$p_list .= '<li>' . htmle(db_fetch_cell_prepared('SELECT name FROM package_repositories WHERE id = ?', [$id])) . '</li>';
$p_array[] = $id;
}
top_header();
form_start('package_repos.php', 'action_confirm');
html_start_box($actions[gnrv('drp_action')], '60%', false, 3, 'center', '');
if (cacti_sizeof($p_array)) {
$save_html = '';
if (gnrv('drp_action') == '1') { // delete
print "<tr>
<td class='textArea'>
<p>" . __n('Click \'Continue\' to delete the following .', 'Click \'Continue\' to delete following Package Repositories.', cacti_sizeof($p_array)) . "</p>
<div class='itemlist'><ul>$p_list</ul></div>
</td>
</tr>";
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __n('Delete Package Repository', 'Delete Package Repositories', cacti_sizeof($p_array)) . "'>" . __esc('Continue') . '</button>';
} elseif (gnrv('drp_action') == '2') { // disable
print "<tr>
<td class='textArea'>
<p>" . __n('Click \'Continue\' to disable the following Package Repository.', 'Click \'Continue\' to disable following Package Repositories.', cacti_sizeof($p_array)) . "</p>
<div class='itemlist'><ul>$p_list</ul></div>
</td>
</tr>";
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __n('Disable Package Repository', 'Disable Package Repositories', cacti_sizeof($p_array)) . "'>" . __esc('Continue') . '</button>';
} elseif (gnrv('drp_action') == '3') { // enable
print "<tr>
<td class='textArea'>
<p>" . __('Click \'Continue\' to enable the following Package Repository.', 'Click \'Continue\' to enable following Package Repositories.', cacti_sizeof($p_array)) . "</p>
<div class='itemlist'><ul>$p_list</ul></div>
</td>
</tr>";
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __n('Enabled Package Repository', 'Enable Package Repositories', cacti_sizeof($p_array)) . "'>" . __esc('Continue') . '</button>';
} elseif (gnrv('drp_action') == '4') { // default
print "<tr>
<td class='textArea'>
<p>" . __('Click \'Continue\' to make the following the following Package Repository the default one.') . "</p>
<div class='itemlist'><ul>$p_list</ul></div>
</td>
</tr>";
$save_html = "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel') . "</button>
<button type='submit' class='ui-button ui-corner-all ui-widget ui-state-active' title='" . __esc('Make Selected Repository Default') . "'>" . __esc('Continue') . '</button>';
}
} else {
raise_message(40);
header('Location: package_repos.php?header=false');
exit;
}
print "<tr>
<td class='saveRow'>
<input type='hidden' name='action' value='actions'>
<input type='hidden' name='selected_items' value='" . htmle(PackageListFilter::encodeSelectedIds($p_array)) . "'>
<input type='hidden' name='drp_action' value='" . htmle(gnrv('drp_action')) . "'>
$save_html
</td>
</tr>";
html_end_box();
form_end();
bottom_footer();
}
function repo_remove(int $id) : void {
db_execute_prepared('DELETE FROM package_repositories WHERE id = ?', [$id]);
db_execute_prepared('DELETE FROM package_repositories WHERE id = ?', [$id]);
}
function repo_disable(int $id) : void {
db_execute_prepared('UPDATE package_repositories SET enabled = "" WHERE id = ?', [$id]);
}
function repo_enable(int $id) : void {
db_execute_prepared('UPDATE package_repositories SET enabled = "on" WHERE id = ?', [$id]);
}
function repo_default(int $id) : void {
db_execute('UPDATE package_repositories SET `default` = ""');
db_execute_prepared('UPDATE package_repositories SET `default` = "on" WHERE id = ?', [$id]);
}
function repo_edit() : void {
global $types;
// ================= input validation =================
gfrv('id');
// ====================================================
if (!ierv('id')) {
$repo = db_fetch_row_prepared('SELECT * FROM package_repositories WHERE id = ?', [grv('id')]);
$header_label = __esc('Package Repository [edit: %s]', $repo['name']);
} else {
$header_label = __('Package Repository [new]');
$repo = [];
}
if (cacti_sizeof($repo)) {
if ($repo['repo_type'] == 1) { // Local directory
$method = 'dirpath';
} else {
$method = 'textbox';
}
} else {
$method = 'textbox';
}
$fields_package = [
'name' => [
'method' => 'textbox',
'friendly_name' => __('Name'),
'description' => __('Enter a meaningful name for this Package Repository.'),
'value' => '|arg1:name|',
'max_length' => '32',
],
'repo_type' => [
'method' => 'drop_array',
'friendly_name' => __('Repository Type'),
'description' => __('Choose what Package Repository type this is.'),
'value' => '|arg1:repo_type|',
'array' => $types,
'default' => '0'
],
'repo_location' => [
'method' => $method,
'friendly_name' => __('Location'),
'description' => __('Either the full path on the Cacti Web Server to the Repository or a URL to the Repository. For example: https://github.com/Cacti/packages/'),
'value' => '|arg1:repo_location|',
'max_length' => '128',
'size' => '120'
],
'repo_branch' => [
'method' => 'textbox',
'friendly_name' => __('Branch'),
'description' => __('For GitHub based repositories, the branch to include such as \'develop\', \'master\', etc.'),
'value' => '|arg1:repo_branch|',
'max_length' => '128',
'size' => '10'
],
'repo_api_key' => [
'method' => 'textbox',
'friendly_name' => __('API Key (optional)'),
'description' => __('For GitHub based repositories, the optional API key required to access the repository.'),
'value' => '|arg1:repo_api_key|',
'max_length' => '128',
'size' => '100'
],
'enabled' => [
'method' => 'checkbox',
'friendly_name' => __('Enabled'),
'description' => __('If this checkbox is checked, you will be able to import packages from this Repository.'),
'value' => '|arg1:enabled|',
'default' => 'on',
],
'default' => [
'method' => 'checkbox',
'friendly_name' => __('Default'),
'description' => __('If this checkbox is checked, this will be the first Repository shown to the user.'),
'value' => '|arg1:default|',
'default' => '',
],
'id' => [
'method' => 'hidden_zero',
'value' => '|arg1:id|'
],
'save_component_repo' => [
'method' => 'hidden',
'value' => '1'
]
];
form_start('package_repos.php');
html_start_box($header_label, '100%', true, 3, 'center', '');
draw_edit_form(
[
'config' => [],
'fields' => inject_form_variables($fields_package, $repo)
]
);
html_end_box(true);
?>
<script type='text/javascript'>
function initPackageType() {
if ($('#repo_type').val() == 0) {
$('#row_repo_branch').show();
$('#row_repo_api_key').show();
} else {
$('#row_repo_branch').hide();
$('#row_repo_api_key').hide();
}
}
$(function() {
initPackageType();
$('#repo_type').change(function() {
initPackageType();
});
});
</script>
<?php
form_save_button('package_repos.php', 'return', 'id');
}
function repos() : void {
global $actions, $item_rows, $types;
// create the page filter
$pageFilter = new CactiTableFilter(__('Package Repositories'), 'package_repos.php', 'fors', 'sess_package_repos', 'package_repos.php?action=edit');
$pageFilter->rows_label = __('Repos');
$pageFilter->render();
$filter = PackageListFilter::fromRequest((int) read_config_option('num_rows_table'));
$sql_where = '';
$sql_params = [];
// form the 'where' clause for our main sql query
if ($filter->hasFilter()) {
$sql_where = ($sql_where != '' ? ' AND ' : 'WHERE ') .
'(name LIKE ? OR repo_branch LIKE ? OR repo_location LIKE ?)';
$sql_params[] = '%' . $filter->filter() . '%';
$sql_params[] = '%' . $filter->filter() . '%';
$sql_params[] = '%' . $filter->filter() . '%';
}
$sql_order = get_order_string();
$sql_limit = ' LIMIT ' . $filter->offset() . ',' . $filter->rows();
$total_rows = db_fetch_cell_prepared("SELECT COUNT(*)
FROM package_repositories
$sql_where",
$sql_params);
$repos = db_fetch_assoc_prepared("SELECT *
FROM package_repositories
$sql_where
$sql_order
$sql_limit",
$sql_params);
$display_text = [
'name' => [
'display' => __('Name'),
'sort' => 'ASC'
],
'repo_type' => [
'display' => __('Type'),
'sort' => 'ASC'
],
'repo_location' => [
'display' => __('Location'),
'sort' => 'ASC'
],
'repo_branch' => [
'display' => __('Branch'),
'align' => 'center',
'sort' => 'ASC'
],
'enabled' => [
'display' => __('Enabled'),
'align' => 'center',
'sort' => 'ASC'
],
'default' => [
'display' => __('Default'),
'align' => 'center',
'sort' => 'ASC'
],
];
$nav = html_nav_bar($filter->paginationUrl('package_repos.php'), MAX_DISPLAY_PAGES, $filter->page(), $filter->rows(), $total_rows, cacti_sizeof($display_text) + 1, __('Package Repositories'), 'page', 'main');
form_start('package_repos.php', 'chk');
print $nav;
html_start_box('', '100%', false, 3, 'center', '');
html_header_sort_checkbox($display_text, $filter->sortColumn(), $filter->sortDirection(), false);
if (cacti_sizeof($repos)) {
foreach ($repos as $repo) {
form_alternate_row('line' . $repo['id'], true);
form_selectable_cell(filter_value($repo['name'], $filter->filter(), 'package_repos.php?action=edit&id=' . $repo['id']), $repo['id']);
form_selectable_cell($types[$repo['repo_type']], $repo['id']);
form_selectable_cell(filter_value($repo['repo_location'], $filter->filter()), $repo['id']);
form_selectable_ecell($repo['repo_type'] == 0 ? ($repo['repo_branch'] != '' ? $repo['repo_branch'] : __('default')) : __('N/A'), $repo['id'], '', 'center');
form_selectable_cell($repo['enabled'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
form_selectable_cell($repo['default'] == 'on' ? __('Yes') : __('No'), $repo['id'], '', 'center');
form_checkbox_cell($repo['name'], $repo['id']);
form_end_row();
}
} else {
print '<tr class="tableRow odd"><td colspan="' . (cacti_sizeof($display_text) + 1) . '"><em>' . __('No Package Repositories Found') . '</em></td></tr>';
}
html_end_box(false);
if (cacti_sizeof($repos)) {
print $nav;
}
// draw the dropdown containing a list of available actions for this form
draw_actions_dropdown($actions);
form_end();
}