Skip to content

Validate hosts including IPv6, keep credentials off the command line, fix script paths (stacked on #44) - #46

Open
somethingwithproof wants to merge 6 commits into
fix/hardening-review-fixesfrom
fix/host-validation-credentials-scripts
Open

somethingwithproof wants to merge 6 commits into
fix/hardening-review-fixesfrom
fix/host-validation-credentials-scripts

Conversation

@somethingwithproof

Copy link
Copy Markdown
Member

Description

Stacked on #44. The base branch of this pull request is fix/hardening-review-fixes, not develop, and it must merge after #44. It builds on #44's POST-only Query Tool, its database-clock scheduling, its Cacti-signature test stubs and its end-to-end harness. After #44 merges, retarget this pull request to develop; no other change is needed.

Four follow-up changes to the WMI client and its scripts. One of them (wmic credentials) is security-relevant: it keeps the Windows password out of the process list.

  • Host validation in the Query Tool: the old check (is_valid_host(), never called) refused every IPv6 address, short names such as win-server, .local names and top level labels over four letters. It is replaced by wmi_valid_host(), which is now used by the tool.
  • Credentials: the wmic transport passed --user and --password as arguments, which any local user can read with ps. It now writes a private authentication file and passes --authentication-file.
  • Script-server files: the Exchange resource and the two scripts pointed at files that do not exist. The paths are corrected and path_wmi is registered as a setting.
  • wmic stderr: it was lost. It is now read apart from stdout, added to the error text only, and a wmic that hangs is stopped.

Related Issue

None open for these. Open issue #15 (Windows hosts) is not touched.

Motivation and Context

Host validation (includes/functions.php, wmi_tools.php):

  • wmi_valid_host() accepts an IPv4 or IPv6 address (filter_var), a bracketed IPv6 address (brackets removed), and a host name under RFC 1123: labels of 1 to 63 letters, digits and hyphens that do not start or end with a hyphen, at most 253 characters, optional trailing dot. It refuses IPv6 zone ids (fe80::1%eth0), names whose last label is all digits (999.1.1.1, 1.2.3), underscores, ports, slashes, spaces, control characters and non-ASCII.
  • The Query Tool validates the host before it builds a command and shows ERROR: The host must be an IPv4 or IPv6 address or a host name otherwise. The message is not translated, like the other ERROR: strings in the client, so the translation template is unchanged by it.
  • Devices polled by the poller or the script server are not validated, so existing installs with device names the strict rule would refuse keep working. Both paths already build the command safely: the host is shell-escaped and follows a literal //, so it cannot be read as an option (tests cover -oProxy, an IPv6 address and the injection-looking inputs), and the PowerShell transport passes it in an environment variable.
  • IPv4 assumptions elsewhere: I searched the plugin for IPv4-only patterns (ip2long, dotted-quad regexes, FILTER_FLAG_IPV4). The only one was is_valid_host(). The plugin stores no host addresses of its own; it uses Cacti's host.hostname (varchar(100), any text), and the host selection is Cacti's device list.
  • Not verified: whether wmic's //host argument accepts an IPv6 address, and whether New-CimSession -ComputerName accepts one. No wmic binary was available (the wmi-client package is not in the Ubuntu 18.04 or 20.04 repositories I could reach), and there is no Windows host here. An IPv6 address is passed as one escaped argument after //, exactly like a name; please try it against a real host.

Credentials (includes/linux_wmi.php):

  • The transport creates a directory with a random name (mode 0700) under the system temporary directory, writes username=, password= and, for DOMAIN\user or DOMAIN/user, domain= into a file created exclusively with a random name (mode 0600), passes --authentication-file=<file>, and removes the file and directory in a finally block, so a failing or timed-out wmic leaves nothing behind.
  • If the directory or file cannot be created or written, it logs a warning and falls back to the old --user and --password arguments.
  • Credentials containing a line break or a NUL byte are refused (the file format is one value per line).
  • Not verified: the file format against a real wmic. It follows the samba credentials file that -A reads (username=, password=, domain=). A password with leading or trailing spaces depends on how that parser trims values. The tests use a stand-in executable that supports -A and records what it received.

Script-server files:

  • templates/resource/script_server/exchange_messages.xml and plugin_wmi_create_resource_xml() named |path_cacti|/scripts/wmi-script.php, which does not exist. They now name |path_cacti|/plugins/wmi/script/wmi-script.php.
  • script/wmi-script.php required ../include/cli_check.php (now ../../../include/cli_check.php) and looked for a wmic inside the plugin that is not shipped. wmi_script.php required ../include/global.php and hard-coded /usr/bin/wmic. Both now use the path_wmi setting, falling back to /usr/bin/wmic.
  • path_wmi was read by the Query Tool and the poller but never registered. It is now a path setting with the default /usr/bin/wmic, so existing installs that have no stored value behave as before. The two new setting strings are added to locales/po/cacti.pot.
  • Not verified: running the root wmi_script.php as a script, because it requires a live Cacti. script/wmi-script.php runs under test through a stand-in wmic.

wmic stderr:

  • stdout is read as the result, stderr separately. On failure the error text is the old ERROR: <stdout> with stderr added after a line break (or ERROR: <stderr> when stdout is empty), capped at 2000 characters. On success stderr is ignored. The timeout is 120 seconds; a wmic that runs longer is killed and reported as ERROR: wmic did not finish within 120 seconds.

How Has This Been Tested?

Environment: PHP 8.4.27 with Xdebug in Docker, Cacti 1.2.x (1.2.32) and Cacti develop (1.3.0) checkouts with composer install as the workflow does it, and MariaDB 11.8.9.

  • New unit tests in four files: WmiValidHostTest (IPv4, IPv6 compressed, expanded and bracketed, zone ids refused, short names, .local, long top level labels, labels at 63 and 64 characters, 253 and 254 character names, trailing dot, empty input, injection-looking input, the command line for -oProxy and an IPv6 address), WmiWmicAuthFileTest, WmiWmicStderrTest and WmiScriptServerTest (resource paths exist, the scripts' require targets exist in the Cacti tree, the setting is registered and keeps other path settings, the script-server function runs through a stand-in wmic using path_wmi).
  • The authentication file tests check: no credentials in the arguments, mode 0600 file and 0700 directory, domain split, passwords with =, quotes, spaces, $(), backslashes, # and non-ASCII, refusal of line breaks and NUL, cleanup after success, failure and timeout, and the fallback when the directory cannot be made, the file cannot be created or the write is short.
  • The end-to-end harness from Harden the collector, cache refresh and WMI pages #44 is extended, not replaced: its fake wmic now supports --authentication-file (it records the file's content, mode and path) and prints a line on stderr on every call, so the cache flows prove that stderr never reaches the parsed rows. New end-to-end checks:
    • the credentials reach wmic in a mode 0600 file that is gone afterwards, for the cache refresh and for the Query Tool, and are not in the arguments;
    • the Query Tool refuses bad host, -oProxy=x, a..b, fe80::1%eth0, 999.1.1.1 and host;id without calling wmic, and passes win-server, [2001:db8::1] (as //2001:db8::1), 10.0.0.5, srv.corp.local and an expanded IPv6 address after the // prefix;
    • script/wmi-script.php and wmi_script.php run from the command line against the real database and the fake wmic (index, key and value, and an unknown query).
  • Pest on Cacti develop without the database: 197 passed and 79 skipped (the end-to-end tests skip without WMI_E2E=1 and a database).
  • CI-order simulation (checkout, composer install as the workflow does it, the pinned Pest harness for 1.2.x, install_cacti, plugin_manage --install --enable on a bare Cacti with MariaDB and no other plugin, the log-writable step, then Pest as a non-root user with WMI_E2E=1 and PHP set to America/New_York, then the workflow's log grep): 275 passed and 1 warning on Cacti 1.2.x, the same on Cacti develop, log grep clean on both. The warning is the existing PruneFilesTest permission test.
  • php tests/bin/patch-coverage.php <clover> origin/fix/hardening-review-fixes 100 (the base of this stack): 100.00 percent (121 of 121 changed measured lines).
  • php tests/bin/e2e-coverage-report.php: every changed executable line, including those in wmi_tools.php, wmi_script.php and script/wmi-script.php, ran in at least one suite.
  • check-i18n-pot.php (pot included in the change) and validate-manifest.php: pass. php -l on every changed file: no errors.

The conflicts with #44 (tests/bootstrap-unit.php, CHANGELOG.md, includes/functions.php, wmi_tools.php, includes/linux_wmi.php) were resolved by keeping #44's version of each shared hunk and adding this change's code beside it. The only stub change left in this branch is a __test_config lookup in read_config_option; #44's row-map stub and its core-matching signatures are used as they are.

Not run: the GitHub workflow itself, PHP 8.2 and 8.3 locally, a real wmic or Windows host, PHPStan and the coding-standards jobs beyond what origin/develop already reports, and Sonar.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation repository accordingly.

Rollback: each commit reverts on its own (the script-path commit also touches locales/po/cacti.pot). No schema change. The new path_wmi setting only appears on the Paths tab; with no stored value the code uses /usr/bin/wmic as before. A host the Query Tool now refuses can still be queried through a device.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ster path_wmi

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…s end to end

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant