Repository navigation
Security hardening: check.php auth, output escaping, output-file confinement, group visibility, format-string guard - #348
Conversation
Map titles and INFOURL values reach the page and the poller-generated HTML straight from the map config, so they are escaped at each output boundary and a duplicated map's title has control characters flattened before it is substituted into the copied, line-oriented config file. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
check.php reports the hostname, kernel, PHP build and ini paths, so the web path now sits behind the Manage Weathermap realm and the page is registered against that realm on install. Existing installs are migrated in the upgrade step, after the two-file widening above it, so a realm still holding the older single-file value is moved across in two steps rather than missed. The command line run is deliberately left open: comparing the web and CLI PHP configurations is what the script is for. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
map_clean_title() is reached through str_replace() on a request variable, so a parameter sent as title[]= arrives as an array and hit the string type declaration. It now yields an empty title instead. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
map_clean_title() only flattens control characters, so an HTML payload in a map title survives into weathermap_maps.titlecache. The three duplicate-map messages interpolated that title through __() and the RSS feed wrote it into XML unescaped. The messages now use __esc(), as the rest of the file already does, and the feed escapes the title once before either use. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
__() runs its own sprintf, so concatenating its result into printf's format made the title part of that format. A title such as "Core 100% Uptime" aborted action=mrss with a ValueError and one holding %s with an ArgumentCountError, truncating the XML for that map and every map after it; the realm needed is only View Weathermaps. The description is now passed as an argument, and the test stub for __() substitutes the way Cacti's does so a broken format string cannot pass unnoticed again. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Registering the page against the realm is not enough on its own. The plugin_realms row is written by the installer and by the upgrade step, and the upgrade step only runs when the recorded version differs from INFO, so an install already sitting on this version kept authentication without authorisation. The page now checks the realm directly and redirects to permission_denied.php, which holds whether or not the migration ran. The test drives check.php under a CGI SAPI with the realm denied and asserts the report is not emitted, rather than matching on source text. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Stage the guard fixtures into the directory tempnam() reserved and clean them up, skip visibly rather than passing when php-cgi is absent, and cover an auth include that declares a colliding function name or emits output before the redirect. Assert that check.php is registered in the Manage Weathermap realm and added to it on upgrade, since include/auth.php refuses any page absent from every realm regardless of the in-page check. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
php-cgi is not packaged for every version in the matrix; ubuntu-latest carries only 8.3, so an apt install failed on 8.1, 8.2 and 8.4. The built-in server reports cli-server, which is enough to take the guard's non-cli branch, so the cases now run on the PHP already under test with nothing to install and no version that silently skips. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The OUTPUTFILE, HTMLOUTPUTFILE, IMAGEOUTPUTFILE and DATAOUTPUTFILE map directives name a file the poller writes. A map config can be authored by a user holding Manage or Edit Weathermap, so the value is untrusted: an absolute path or one containing a directory component let the poller, and the mgmt rebuild request that runs in the web context, write or overwrite an arbitrary file as its own user. Resolve each target through weathermap_confine_output_path and accept only a bare filename inside the plugin output directory; anything else is logged (WMPOLL08) and skipped. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
get_allowed_weathermaps() built the viewer's map list without the uag.enabled = 'on' filter that is_weathermap_allowed() applies, so a member of a disabled group kept seeing that group's maps (and their generated HTML) in the list and cycle views. Gate the group-membership branch on enabled groups to match the per-map check. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
mysprintf() applies a map label, title or note as a printf format string. Those values come from the map config, so a stray or unknown conversion specifier made sprintf() throw under PHP 8 (ArgumentCountError or ValueError). mysprintf() runs inside the poller's map loop with no handler above it, so one bad label aborted the entire poll. Fall back to the literal format text instead of throwing. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
ee15f4e to
15f1469
Compare
|
Checked
After the fixture corrections and the separate #341/#347 browser-test adjustment, the combined batch passed 660 PHP tests and 33 browser test files. Those results don't resolve the confinement issue above. |
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…names An existing destination that is a symbolic link, or a file with a second hard link, is now refused, so writing a map output cannot change a file outside output/. File names may contain dots and spaces and use any letter case in the extension, a path whose directory is output/ is still accepted, and a script extension in the middle of a name is refused. The configuration reference lists the accepted names. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
|
Thanks for the careful check. Addressed in d1a589e and c1e681a:
All 405 PHP tests pass on PHP 8.3, including new cases for each of the above. |
The management page renders at include time, so tests had to pull the helper out of its source with eval(). Giving it its own file lets the page and every test load the real function with require_once. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Drive weathermap_run_maps() over fixture maps whose HTML, image and data output files point inside and outside output/, and render a tree item whose title holds markup. check.php is driven through PHP's built-in server, which Clover cannot measure, so it joins the coverage allowlist. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
alcatron
left a comment
There was a problem hiding this comment.
Rechecked 6624e72. The existing symlink, dangling symlink, hard-link and directory cases are now rejected. I also checked dotted filenames, spaces, uppercase extensions and paths inside output/; those are accepted, while outside paths and script-extension names are refused. The map-title fixtures now load the helper correctly.
The eight active PRs together pass 648 PHP tests and all 33 browser test files, without our previous test fixes. The specific issues I raised are addressed.
Merge housekeeping: this conflicts with the batch changelog from #333 and the catalogue creation-date header from #336. I retained both sets of changelog entries and the newer catalogue date in the test checkout; no code or test edits were needed. Minor nit: please preserve the catalogue version and Cacti translator/team metadata when resolving the header, rather than the regenerated blank version/placeholders.
# Conflicts: # CHANGELOG.md # locales/po/cacti.pot
…njection map_clean_title() strips control characters from a duplicate-map title but not its length. ReadConfig() parses the config with fgets(\, 4096), so a title long enough to push the TITLE line past that chunk boundary has its overflow read as a separate directive (e.g. a trailing INCLUDE/WIDTH). map_duplicate() now rejects a cleaned title longer than 4088 bytes before writing the config, matching newMap(), reusing its existing message. Adds a subprocess regression (MapDuplicateTitleBoundaryPayload/Test) covering the 4088-byte accept and the 4089-byte and injected-directive reject at the chunk boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressed the "previously missed" title-length findingThe latest review overview flagged a HIGH issue in otherwise-unchanged code: a duplicated-map title long enough to cross
Verification
|
…tle guard The title-length pre-check assumes the global TITLE sits at column 0, as WriteConfig() emits it. map_duplicate() substitutes the new title into the existing config with str_replace(), which preserves any leading whitespace on the source TITLE line, so a <=4088-byte title could still produce a line past ReadConfig()s fgets(4096) boundary and have its tail parsed as a directive. After substitution, re-check the written TITLE lines and reject any that exceed 4094 bytes, reusing the existing message. Extends the boundary regression with an indented-source case proving a title that clears the pre-check is still rejected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressed the preserved-indentation follow-upThe latest review overview flagged for human assessment that preserved TITLE indentation could bypass the length guard. That's a valid edge, and I've closed it in Why it was reachable: the length pre-check ( The fix adds an authoritative post-substitution check: after the Verification
I also replied to and resolved the SonarCloud |
… boundary
The duplicate-title substitution is a whole-file str_replace of "TITLE {old}",
so it also rewrites any comment or label line that contains that string. A
comment such as "# TITLE {old}" overflowed past ReadConfig()'s fgets(4096)
boundary would have its split tail parsed as a directive, escaping the leading
'#', bypassing a TITLE-only guard. Validate every line the substitution
lengthens (strpos marker + resulting length > 4094), not just TITLE lines,
before writing the config.
Adds a comment-line regression alongside the indented-source one.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressed the comment-line overflow follow-upThe review overview flagged that title substitution can overflow non-TITLE lines (e.g. comments) past the read limit. Valid — closed in The substitution is a whole-file The guard now validates every line the substitution lengthens: for each source line containing the Verification
|
The patch-coverage gate flagged three defensive lines in wm_href_is_safe() that the string-only call site never exercises. Collapse the default_charset fallback to a single always-run expression, and add direct unit cases for the non-string and whitespace-only/control-only inputs so the early returns are covered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the per-line chunk-boundary check out of map_duplicate() and into a new map_duplicate_title_overflows_line() helper in lib/map-title.php. The behaviour is unchanged: the copy is still rejected when substituting the title would push any rewritten line (an indented TITLE line, or a comment/label that also holds "TITLE <old>") past ReadConfig()'s fgets(4096) boundary. This replaces the eval()/subprocess test fixture (which SonarCloud flagged as dynamic code execution) with a direct unit test of the helper, covering the column-0, indentation, comment-line, marker-absent and non-string cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The three near-identical per-directive extension tests tripped SonarCloud new-code duplication. Collapse them into a single dataset-driven case (allowed extensions, accepted names, rejected names) with no change in coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ImageMapEscapingTest carried a duplicated block (SonarCloud new-code duplication). Extract a wm_area_html() helper for the repeated area setup and merge the drop-scheme and keep-href dataset tests into one classification case keyed on an expected-safe flag. Same assertions and coverage, no duplicated block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
CI green + follow-up review items addressedAll checks now pass (PHP 8.2–8.6 integration, CodeQL, jsdom, both SonarCloud gates). Summary of the follow-ups since the last review round: Review findings (all resolved threads)
CI fixes
Validation
|



Security hardening for the Weathermap plugin. Each change is covered by a test and was verified against a live Cacti 1.2.31 instance on PHP 8.4 (Docker).
Changes
check.phpbehind the Manage Weathermap realm. The pre-install report disclosed the PHP version, host name, kernel, ini paths and loaded extensions to any anonymous requester. It now includesauth.phpand is registered in the Manage realm (install and upgrade) soauth.phpauthorizes it; the CLI run stays open for the install check.html_escape()/__esc(), and a%in a title no longer breaks the feed.OUTPUTFILE,HTMLOUTPUTFILE,IMAGEOUTPUTFILEandDATAOUTPUTFILEare resolved and accepted only as a bare filename inside the pluginoutput/directory, so a map config can no longer make the poller (or the mgmt "rebuild now" path) write or overwrite an arbitrary file.get_allowed_weathermaps()now filters onuag.enabled = 'on', matchingis_weathermap_allowed(), so members of a disabled group no longer see that group's maps in the list and cycle views.ArgumentCountError/ValueErrorthat aborted the whole poller cycle on PHP 8.Verification
tests/Security/andtests/Unit/cover each change.check.phpserves the report only to Manage users (anonymous → login, view-only → permission denied); an absolute/..output-file path is rejected while a bare filename still writes tooutput/; a disabled-group member no longer sees the map; a malformed label no longer aborts the poll.Corresponding details are tracked privately through the project's security advisory process.