Skip to content

security: batch 12 (develop) - GHSA-4qx8 timing, GHSA-54fg SSRF, GHSA-5v3j data-input RCE, GHSA-vhh2 confirm - #8237

Open
TheWitness wants to merge 12 commits into
developfrom
security-batch-12
Open

TheWitness wants to merge 12 commits into
developfrom
security-batch-12

Conversation

@TheWitness

@TheWitness TheWitness commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Security batch 12 (develop)

Consolidated fixes for four privately-reported advisories. Each has a guard
under tests/Unit/Security/ in a vulnerability-class subfolder, plus a CHANGELOG
entry.

GHSA Issue Fix Scope
GHSA-4qx8-jj2h-p7q2 Timing side-channel in the 2FA bypass-cookie and password-reset token comparisons hash_equals() instead of === / != in auth_2fa.php and auth_resetpassword.php develop-only (files absent in released 1.2.x)
GHSA-54fg-q9h9-88mm SSRF via the package-repo Direct-URL/GitHub manifest fetch (raw file_get_contents, TLS verification disabled) Route both the save-time probe (package_repos.php::form_save) and the import-time read (package_import.php::get_repo_file) through cacti_http() (scheme/host validation, private-range block, DNS pinning, TLS verify); Bearer token via the headers option develop-only (package_repos.php absent in released 1.2.x)
GHSA-5v3j-wcrr-jxjg Authenticated OS command injection: a Data Input template that wraps a <field> placeholder in quotes ("<arg1>") — or embeds it in a larger quoted word ("prefix<arg1>") — neutralizes cacti_escapeshellarg()'s own quoting, so $(...)/backticks in a realm-3 user's field value execute on the next poll substitute_script_path() is now a single-pass, quote-aware scanner: it sheds an exact wrapping pair and, for a token embedded inside a larger quoted word, briefly closes the template's quoting around the already-escaped value so it cannot be re-interpreted (single-pass fq9x behaviour retained) released ≤ 1.2.31 and develop — this is the develop fix; the 1.2.x backport is #8238
GHSA-vhh2-gghg-whjg OS command injection via the spikekill poller --rrdfile path Already fixed in #7235 (cacti_escapeshellarg($f)); this batch adds a confirmation regression test only released ≤ 1.2.31, fixed in develop + 1.2.x

Tests

Regressions live under the existing vulnerability-class layout:

  • tests/Unit/Security/Auth/TwoFaCookieAndResetTokenConstantTimeTest.php — GHSA-4qx8 constant-time compares.
  • tests/Unit/Security/Ssrf/PackageRepoManifestFetchTest.php — GHSA-54fg: both the save-time and import-time repo fetches route through cacti_http() and drop raw file_get_contents/verify_peer.
  • tests/Unit/Security/ShellInjection/DataInputQuotedPlaceholderTest.php — GHSA-5v3j: a double-quoted placeholder sheds its quotes, a token embedded in a larger quoted word keeps the value safely quoted, bare/unknown tokens are unchanged, and the single-pass fq9x behaviour is retained.
  • tests/Unit/Security/ShellInjection/SpikekillRrdfileEscapeTest.php — GHSA-vhh2: the --rrdfile path is escaped.

tests/Unit/Security/Batch11SecurityRegressionTest.php is updated to assert the
quote-aware scanner markers rather than the obsolete regex.

Deliberately not in this batch

  • GHSA-8v82-c8w3-jvr7 (state-changing GET / CSRF guard bypass) remains the
    tracked develop CSRF epic. A correct fix couples the guard change with
    converting the affected action links to POST (the "guard and links move
    together" rule from batch 11); a guard-only patch would regress the plugins and
    clog UIs, so it is intentionally excluded here.

CVE status

  • GHSA-5v3j: accepted, CVE requested.
  • GHSA-vhh2: tracked under GHSA-227p-wv52-pjg5 (duplicate report closed).
  • GHSA-4qx8 / GHSA-54fg: develop-only (never released) - no CVE per policy.

Verified: php -l on all changed files; the quote-aware scanner's output was
checked under /bin/sh to confirm the escaped value stays a literal argument for
every token shape; behavioral + source assertions pass (Pest runs in CI).

TheWitness added 4 commits October 8, 2026 18:58
…(GHSA-4qx8-jj2h-p7q2)

auth_2fa.php used === to compare the 2FA bypass-cookie HMAC and
auth_resetpassword.php used != for the reset-token check; both are
byte-by-byte and leak timing. Switch to hash_equals() so the comparison
is constant-time. Develop-only paths (no released 1.2.x equivalent).
…http (GHSA-54fg-q9h9-88mm)

package_repos.php fetched the package.manifest for the GitHub and
Direct-URL (repo_type 0/2) repository types with raw file_get_contents()
- the Direct-URL path additionally disabled SSL verification - which
bypassed the hardened cacti_http() client and allowed SSRF (cloud
metadata, internal host/port probing) plus MITM. Both paths now use
cacti_http(), which restricts the scheme to http/https, blocks
private/loopback/link-local targets, pins DNS to prevent rebinding, and
verifies TLS. The GitHub Bearer token is passed via the headers option.
Develop-only (package_repos.php does not exist in released 1.2.x).
…ens (GHSA-5v3j-wcrr-jxjg)

Field values are escaped with cacti_escapeshellarg(), which wraps them
in single quotes. When a Data Input template author placed the <field>
placeholder inside a matched pair of quotes (e.g. "<arg1>"), those
surrounding quotes made the value's own quoting literal, so $(...) or
backticks in the value executed via the shell on the next poll - a
realm-3 user reaching OS command execution, bypassing the CVE-2026-39902
hardening and not addressed by the single-pass fq9x fix.

substitute_script_path() now consumes one matched pair of surrounding
quotes when a token resolves, so the escaped value's own quoting is
authoritative. Unresolved tokens keep their literal form (including any
quotes), and the single-pass behaviour that fixed GHSA-fq9x is retained.
Affects released 1.2.x (<= 1.2.31) and develop; this is the develop fix,
to be backported to 1.2.x.
…fixes

Add tests/Unit/Security/Batch12SecurityRegressionTest.php guarding the
batch-12 sinks (GHSA-4qx8, GHSA-54fg, GHSA-5v3j) with a behavioral proof
that a double-quoted Data Input placeholder sheds its quotes, plus a
confirmation test that poller_spikekill.php escapes --rrdfile
(GHSA-vhh2, already fixed in #7235). List the four GHSAs in the CHANGELOG.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 23:12
@TheWitness
TheWitness requested a review from a team as a code owner October 8, 2026 23:12
@TheWitness
TheWitness requested review from cigamit and removed request for a team October 8, 2026 23:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Remote import SSRF and quoted-placeholder command injection remain reachable, and an existing regression test will fail.

5 open findings
What changed in this PR

Hardens authentication comparisons, repository fetching, and data-input command substitution for four security advisories.

Changes:

  • Uses constant-time authentication token comparisons.
  • Routes repository probes through the SSRF-hardened HTTP client.
  • Adds quote-aware command substitution and security regression tests.
File Description
auth_2fa.php Uses hash_equals() for 2FA cookies.
auth_resetpassword.php Uses constant-time reset-token comparisons.
package_repos.php Hardens repository reachability probes.
lib/​functions.php Changes data-input token substitution.
tests/​Unit/​Security/​Batch12SecurityRegressionTest.php Adds advisory regression coverage.
CHANGELOG Documents the security changes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/functions.php Outdated
Comment thread package_repos.php
Comment thread lib/functions.php Outdated
Comment thread tests/Unit/Security/Batch12SecurityRegressionTest.php Outdated
Comment thread tests/Unit/Security/Batch12SecurityRegressionTest.php Outdated
TheWitness added 3 commits October 8, 2026 19:33
…re substitute_script_path

The GHSA-5v3j quote-shedding changed substitute_script_path()'s regex
and capture group, which the batch-11 fq9x test pinned verbatim. Update
those two assertions to the quote-aware pattern marker and $matches[2];
the single-pass guarantee the test covers is unchanged.
The hash_equals() guard left $hash inferred as non-empty-array|true, so
$hash['user_id'] tripped PHPStan level 6. Guard with is_array() instead
of a truthy check; db_fetch_row_prepared returns false for no row, so
behavior is unchanged.
TheWitness pushed a commit that referenced this pull request Oct 8, 2026
…5v3j) + spikekill confirm (GHSA-vhh2)

GHSA-5v3j-wcrr-jxjg: a Data Input template that wraps a <field>
placeholder in a matched pair of quotes ("<arg1>") neutralized the
value's own cacti_escapeshellarg() single-quoting, letting $(...) or
backticks in a realm-3 user's field value execute on the next poll
(bypassing the CVE-2026-39902 hardening). substitute_script_path() now
consumes one matched pair of surrounding quotes when a token resolves,
so the escaped value's quoting stays authoritative; unresolved tokens
keep their literal form and the single-pass GHSA-fq9x behaviour holds.

GHSA-vhh2-gghg-whjg: add a confirmation regression test that the
spikekill poller escapes the --rrdfile path (fixed in #7235).

Tests: tests/Unit/Security/DataInputQuotedPlaceholderTest.php (source +
behavioral) and tests/Unit/Security/SpikekillRrdfileEscapeTest.php.
Backport of the develop batch-12 fix (PR #8237).
…cti_http (GHSA-54fg)

get_repo_file() still used raw file_get_contents() for the GitHub and Direct-URL repo types, bypassing the SSRF hardening added to form_save(). Route both remote branches through cacti_http() (scheme/host validation, private-range block, DNS pinning, TLS verify; Bearer token via the headers option) and add a regression assertion covering the import sink.
…A-5v3j)

The matched-pair quote shed only neutralised a token the template wrapped
exactly ("<arg1>"). A token embedded in a larger quoted word ("prefix<arg1>",
"<arg1>suffix", "<a><b>") kept the outer double quotes, so backticks/$(...) in
the already-escaped value stayed live - the bypass Copilot flagged.

Replace the regex with a single-pass shell-quote-state scanner: it still sheds
an exact wrapping pair, but for any token inside a larger quoted word it briefly
closes the template's quoting around the escaped value so it cannot be
re-interpreted. Values are never re-scanned (single-pass, GHSA-fq9x retained).

Updates the batch-11/12 source assertions, adds prefix/suffix/mixed regression
cases, and drops the Batch12 eval fallback (the unit bootstrap already loads
lib/functions.php via include/global.php).
bmfmancini
bmfmancini previously approved these changes Oct 9, 2026
TheWitness added a commit that referenced this pull request Oct 9, 2026
…A-5v3j)

Backport of the develop fix (#8237). The matched-pair quote shed only
neutralised a token the template wrapped exactly ("<arg1>"). A token embedded in
a larger quoted word ("prefix<arg1>", "<arg1>suffix", "<a><b>") kept the outer
double quotes, so backticks/$(...) in the already-escaped value stayed live.

Replace the regex with a single-pass shell-quote-state scanner: it still sheds
an exact wrapping pair, but for any token inside a larger quoted word it briefly
closes the template's quoting around the escaped value so it cannot be
re-interpreted. Values are never re-scanned (single-pass, GHSA-fq9x retained).

Updates the source assertions in DataInputQuotedPlaceholderTest and
ShellInjection/ShellInjectionRegressionTest, and adds prefix/suffix/mixed
regression cases. The eval-extract fallback is kept here because the 1.2.x unit
bootstrap does not load lib/functions.php (unlike develop).
…olders

Split tests/Unit/Security/Batch12SecurityRegressionTest.php into behavior-named
files under the existing vulnerability-class layout, per the review feedback:

  Auth/TwoFaCookieAndResetTokenConstantTimeTest.php   GHSA-4qx8
  Ssrf/PackageRepoManifestFetchTest.php               GHSA-54fg
  ShellInjection/DataInputQuotedPlaceholderTest.php   GHSA-5v3j
  ShellInjection/SpikekillRrdfileEscapeTest.php       GHSA-vhh2

Each file reaches the repo root via dirname(__DIR__, 4) for its one
subfolder-deep location. No assertion changes; pure relocation.
…_script_path

Keep the develop fix in sync with the 1.2.x backport (#8238): substitute_script_path()
only sheds/closes the template's quotes for self-quoting (cacti_escapeshellarg) values.
Trusted raw path_* tokens are substituted in place so a quoted "<path_php_binary>" keeps
its quotes for the shell and a bare <path_cacti>/scripts/x.php stays unquoted for the PHP
script server, which resolves the first token as a filesystem path. Reformatted per
php-cs-fixer and added a raw-path-token regression test.
…itute_script_path

Mirror the 1.2.x fix (#8238): the scanner's backslash branches consumed the next
character unconditionally, so a legacy/custom template token such as \<arg> never
reached the matcher and the trailing cleanup stripped it to a bare backslash. Both
branches now only pair the backslash with a following quote/backslash (so an escaped
quote still cannot toggle quote-state); any other character - notably a <token> start -
leaves the backslash on its own and is processed on the next iteration. Reformatted per
php-cs-fixer and added a backslash-before-token regression test.

This branch has not been deployed

No deployments
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.

3 participants