Skip to content

security: harden SQL, output escaping, and unserialize paths - #773

Open
somethingwithproof wants to merge 41 commits into
Cacti:developfrom
somethingwithproof:security/consolidated-hardening-20260516
Open

security: harden SQL, output escaping, and unserialize paths#773
somethingwithproof wants to merge 41 commits into
Cacti:developfrom
somethingwithproof:security/consolidated-hardening-20260516

Conversation

@somethingwithproof

@somethingwithproof somethingwithproof commented May 17, 2026

Copy link
Copy Markdown
Member

Consolidates the hardening from #766, #767, #769, #770, #771 and #772 into one diff.

Security

  • Bulk form actions in notify_lists.php and notify_queue.php use prepared statements with IN (?,?,?) placeholders instead of array_to_sql_or() and string concatenation.
  • get_allowed_thresholds() and get_allowed_threshold_logs() bind $graph_id rather than interpolating it. Callers supply values for any ? in their own $sql_where via a new $sql_params argument, appended before the $graph_id placeholder.
  • The rfilter value goes through db_qstr_rlike() where Cacti provides it. That helper is core's remediation for GHSA-69gg-xrh3-gp82 and bounds the operand as well as quoting it; it arrived in 1.2.31, so the plugin falls back to plain quoting on the 1.2.25 it declares support for.
  • Values substituted into trigger commands are quoted with cacti_escapeshellarg().
  • page, id and drp_action are escaped where they are printed into hidden inputs, and AJAX filter parameters are wrapped in encodeURIComponent().
  • The RPN evaluator no longer uses eval(). Operators dispatch through a switch; operands were already validated numeric and the operator set was already closed, so the results are unchanged.

Bugs this uncovered

Writing tests that run the code rather than grep it turned up four defects, all fixed here:

  • Every bulk action on the Notification Lists page did nothing. get_filter_request_var() stores drp_action as an int while the allowlist held strings, so the strict in_array() rejected all of them and each action redirected without touching the database. This was introduced by 9eee922 on this branch.
  • Bulk writes were discarded on MySQL. Cacti's db_commit_transaction() gates the commit on SELECT @@in_transaction, which only MariaDB defines; on MySQL the query errors and the commit is skipped, so the open transaction is rolled back at disconnect. Verified against mysql:8 and mariadb:10.6. The plugin now issues the transaction statements directly, which behaves identically on both. Worth a separate core fix, since these helpers have no other caller in 1.2.31.
  • 0 0 / corrupted the RPN stack. The zero-divided-by-zero case broke out of the operator switch before pushing its result, so the stack lost two entries and gained none.
  • % by zero was a fatal, and SQRT of a negative or LOG of zero pushed NAN/-INF. Those compare false against every bound, so a breach went unnoticed. Both now raise the expression's error flag.
  • Inline trigger commands logged nothing. thold_process_command_output() was passed the topic 'thold', which matches none of its branches.

Deleting a notification list also no longer skips soft-deleted devices, so a device that is later restored cannot come back pointing at a list that no longer exists.

Tests

The previous suite read the plugin's source as text and asserted on substrings, so it could not tell a working fix from a comment; every one of the bugs above passed it. It is replaced with tests that run the code against a recording stand-in for the Cacti framework functions.

145 tests. Every line this branch changes is covered, which CI enforces per pull request — whole-file coverage would be meaningless when most of the plugin only runs inside a live Cacti.

The suite runs on PHP 8.1 in Docker, matching the oldest interpreter the integration matrix covers. composer test:docker runs locally exactly what CI runs.

Compatibility

Checked against release/1.2.31 and release/1.2.25: no PHP 8-only syntax, and every core function, constant and signature the diff relies on exists in both.

Copilot AI review requested due to automatic review settings May 17, 2026 07:28

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR hardens the Thold plugin against multiple security issues (SQL injection via RLIKE concatenation and bulk-action concatenation, XSS via unescaped request vars in URLs and HTML, and unsafe deserialization) and corrects a couple of variable-name bugs in thold_command_execution(). It also introduces a Pest-based test suite with stubs/bootstrap for running outside Cacti, plus PHP lint and PHP 7.4 compatibility smoke tests.

Changes:

  • Replace raw RLIKE/SQL string concatenation with db_qstr() and parameterized queries (db_execute_prepared, db_fetch_assoc_prepared, db_fetch_cell_prepared), and add $sql_params to get_allowed_thresholds/get_allowed_threshold_logs.
  • Wrap URL parameters in encodeURIComponent, escape get_request_var('page') with html_escape in hidden inputs, replace cacti_unserialize(stripslashes(...)) with sanitize_unserialize_selected_items, and add drp_action whitelist + per-branch input validation in notify_lists.php.
  • Fix two thold_set_environ calls that incorrectly passed trigger_cmd_high in the low/norm branches; add Pest test suite, bootstrap stubs, composer.json, and phpunit.xml.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
notify_lists.php Convert bulk SQL to prepared statements; add drp_action whitelist and request var filters; encode URL params
thold_functions.php Add $sql_params, switch to prepared SELECTs; fix trigger_cmd_low/norm environ args; annotate eval/exec with nosemgrep
thold_graph.php Use db_qstr for RLIKE; html_escape page var; encodeURIComponent URL params
thold.php Use db_qstr for RLIKE; encodeURIComponent URL params
thold_templates.php encodeURIComponent on filter
thold_webapi.php Switch to sanitize_unserialize_selected_items; encode URL params
notify_queue.php encodeURIComponent URL params
setup.php Use cacti_sizeof instead of count
tests/* New Pest test suite, bootstrap stubs, smoke/security tests
composer.json, phpunit.xml Test tooling config
Comments suppressed due to low confidence (1)

notify_lists.php:21

  • $actions and $assoc_actions are not visible in this diff hunk, but the existing code branches further below dispatch on drp_action == '1' / '2' for at least three different contexts (lists, associate, templates, tholds) which may use overlapping numeric keys. Combining them with $actions + $assoc_actions only validates membership in the union of keys, which is fine, but please confirm that all four save_* sections (save_list, save_associate, save_templates, save_tholds) only ever receive drp_action values present in one of those two arrays — otherwise legitimate actions will hit raise_message(40) and redirect. If save_templates/save_tholds use a different action map, they will be rejected here.
 | http://www.cacti.net/                                                   |

Comment thread notify_lists.php Outdated
Comment thread notify_lists.php Outdated
Comment thread tests/Security/RlikeInjectionTest.php Outdated
Comment thread tests/Smoke/PhpSyntaxTest.php Outdated
Comment thread thold_functions.php
Comment thread thold_functions.php
Comment thread tests/bootstrap.php Outdated
Comment thread notify_lists.php
@somethingwithproof

Copy link
Copy Markdown
Member Author

@TheWitness @netniV — requesting a final review on this consolidated security hardening PR when you have a moment.

Latest push (3405133) hardens the two remaining items in thold_functions.php:

  1. RPN evaluator — removed both eval() sinks in thold_expression_math_rpn(). Replaced with native switch dispatch:

    • Binary operators (+ - * / % ^): direct computation; + 0 coercion and (int) casts preserve the integer semantics eval() applied to % and ^.
    • Unary functions (SIN/COS/TAN/ATAN/SQRT/FLOOR/CEIL/DEG2RAD/RAD2DEG/ABS/EXP/LOG): dispatch to the native math functions, plus a newly added is_numeric() guard that was previously missing on this path. Operands were already validated numeric and operators/functions already whitelisted, so legitimate RPN behavior is unchanged.
  2. trigger_cmd_* execution — shell escaping. thold_replace_threshold_tags() gained a $shell flag; only the three trigger_cmd_high/low/norm exec() callers pass true, which wraps device/user-derived string tags (<DESCRIPTION>, <HOSTNAME>, <LOCATION>, <SITE>, <THRESHOLDNAME>, <DSNAME>, <NOTES>, <DNOTES>, <DEVICENOTE>, <EXTERNALID>) in cacti_escapeshellarg(). Email/HTML callers keep the default $shell = false and are unaffected. Numeric/computed tags remain unescaped.

Validation: php -l clean, PHP 7.4-safe (no PHP 8-only syntax), targeting 1.2.31-idiomatic helpers.

Known follow-ups:

  • The branch is currently conflicting with develop and needs a rebase — happy to rebase before merge.
  • thold_expand_string()'s |...| substitutions into the command line remain unescaped (lower risk: admin-set graph titles / DS names); can address in a follow-up if you'd like it in scope.

@somethingwithproof

Copy link
Copy Markdown
Member Author

Thanks for the automated review. Status of each point below — most were already handled in earlier commits on this branch; the two outstanding items are addressed in 0e2c54e.

Addressed in 0e2c54e:

  • notify_lists.php — SQL string quoting consistency (the notify_warning_extra/notify_extra clearing queries): converted both from double-quoted PHP strings with escaped \"\" to single-quoted strings, matching the surrounding prepared queries in the same block. Verified the rendered SQL is byte-identical (SET … = '' … AND (tt.notify_templated = "" OR tt.notify_templated IS NULL)), so this is a pure readability change.
  • thold_functions.phpexec() env side-effect: the putenv()/thold_set_environ() note now appears on all three trigger_cmd_high/low/norm branches (it previously existed only on the low branch).

Already handled earlier on this branch:

  • thold_functions.php$sql_params contract: get_allowed_thresholds() / get_allowed_threshold_logs() both carry a docblock noting $sql_where may contain ? placeholders supplied via $sql_params.
  • tests/Smoke/PhpSyntaxTest.php — unquoted $phpBin: the lint command already wraps both $phpBin and $file in escapeshellarg().
  • tests/Security/RlikeInjectionTest.php — readability: the asserted pre-fix patterns are expressed as nowdocs with an explanatory comment.
  • tests/bootstrap.php — stub drift: the sanitize_unserialize_selected_items() stub carries a KEEP IN SYNC with Cacti core comment.
  • deleted = "" behavioral change: documented in the PR description (“What” section) — the original SET thold_host_email = 0 AND deleted="" was a bug; deleted = "" is now correctly in the WHERE clause.

No change (with rationale):

  • notify_lists.phpheader('Location: …'); exit; idiom: this is the established Cacti 1.2.x pattern, used consistently in this same file (e.g. the other form_actions() redirects) and throughout core, so it was left as-is rather than switching to header_redirect().
  • drp_action allowlist coverage (low-confidence note): confirmed all four save_* contexts (save_list, save_associate, save_templates, save_tholds) dispatch only on drp_action 1/2, so the $actions + $assoc_actions union ({1,2}) fully covers them. Type alignment via array_map('strval', …) is noted in the PR description.

php -l is clean on both changed files; changes remain PHP 7.4-safe. Note the branch is still CONFLICTING with develop (single-file conflict in thold_functions.php) — I'll rebase before merge.

@somethingwithproof

Copy link
Copy Markdown
Member Author

@TheWitness @netniV — this is ready for a final review when you have a moment.

Status

  • The automated review feedback is fully addressed as of 0e2c54e (per the itemized comment above): SQL quoting consistency in notify_lists.php, exec() env-side-effect comments across all three trigger_cmd_* branches, plus confirmation that the already-handled items (docblocks, test escaping, stub-sync note, the deleted = "" WHERE fix) are in place.

CI heads-up

  • The Plugin Integration Tests workflow has not run on the latest commits — the current head (0e2c54e) has no check runs, and the most recent runs on this branch are from mid-May (on the older commits) and failed. On fork PRs this repo gates workflow runs on maintainer approval, so CI can't validate the current head until one of you approves the run. Could you approve/trigger it? I'll promptly fix anything it surfaces.

Before merge

  • The branch is still conflicting with develop (a single-file conflict in thold_functions.php). I'll rebase once you've had an initial look, so the final review reflects the state you'd merge.

Thanks!

@somethingwithproof

Copy link
Copy Markdown
Member Author

Update: merged latest develop (#774) — now mergeable, hardening verified

This PR is now mergeable against develop as of 592cc7e.

Conflict resolution with #774

develop #774 ("fix 13 logic bugs in RPN evaluation, SNMP traps, and notification handling") rewrote the same thold_expression_math_rpn() evaluator this PR hardens. I merged develop in and resolved thold_functions.php to keep both:

Semantics are identical to the previous eval() (binary computes $v2 <op> $v1 with (int) casts on %/^; unary dispatches sin()/cos()/… — PHP function names are case-insensitive). The two queued exec() annotations were aligned to develop's wording (the call itself is unchanged).

Security changes recap

  • RPN evaluator: eval() → native switch (removes the code-execution sink); added the missing is_numeric guard on the unary path.
  • trigger_cmd_* exec: device/user-derived tags escaped via cacti_escapeshellarg() (new $shell flag on thold_replace_threshold_tags()); email/HTML callers unaffected.
  • Earlier: prepared statements + db_qstr for RLIKE, encodeURIComponent on AJAX filters, sanitize_unserialize_selected_items, and transaction/atomicity fixes.

Validation

  • Pest suite: 53 passed / 0 failed (Security + Smoke), incl. TriggerCmdRegressionTest, RlikeInjectionTest, UnserializeHardeningTest, XssEscapingTest, Php74CompatibilityTest, and php -l across all plugin files.
  • No eval() sinks remain; PHP 7.4-safe.

Known follow-ups (not in this PR)

@TheWitness @netniV — this is ready for review whenever you have a moment; happy to adjust anything.

@somethingwithproof

Copy link
Copy Markdown
Member Author

Re-verified: branch is up to date with develop (no rebase needed), all 4 Integration Test checks currently pass. All 8 Copilot review threads on this PR are already marked resolved. No changes needed.

@TheWitness TheWitness left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use Cacti's composer.json in testing.

bmfmancini
bmfmancini previously approved these changes Aug 17, 2026

@TheWitness TheWitness left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No composer.json in plugins.

browniebraun
browniebraun previously approved these changes Aug 17, 2026
somethingwithproof added a commit to somethingwithproof/plugin_thold that referenced this pull request Aug 17, 2026
Same harness as Cacti#773 and Cacti#788, so whichever lands first the others merge cleanly.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Thomas Vincent and others added 19 commits August 17, 2026 14:25
… env comments)

notify_lists.php: convert the two notify_warning_extra/notify_extra clearing
queries from double-quoted PHP strings (with escaped double quotes) to
single-quoted strings, matching the surrounding prepared queries in the same
block. The rendered SQL is byte-identical (verified), so this is purely a
readability/consistency change that removes the escape-character confusion the
review flagged.

thold_functions.php: mirror the putenv() side-effect comment onto the
trigger_cmd_high and trigger_cmd_norm branches (it previously existed only on
trigger_cmd_low) so all three exec() paths document that thold_set_environ()
populates the environment exec() inherits.
sanitize_unserialize_selected_items rejects nested wizard arrays and
broke thold_new_graphs_save. Use the graphs sanitizer when available,
else allowed_classes => false with structure checks.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The previous suite read the plugin's source as text and asserted on
substrings, so it could not distinguish a working fix from a comment. This
runs the code instead: tests/Support/CactiStub.php records and programs the
Cacti framework functions the plugin calls, and PHPUnit runs against PHP 8.1
in Docker to match the oldest interpreter the CI matrix covers.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Modulo by zero raised an uncaught DivisionByZeroError, and SQRT of a negative
or LOG of zero pushed NAN or -INF, which compares false against every bound so
the breach went unnoticed. Zero divided by zero broke out of the operator
switch before pushing its result, leaving the stack short by two.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
db_qstr_rlike() is core's remediation for GHSA-69gg-xrh3-gp82: on top of
quoting it bounds the operand and strips the alternation characters that made
the pattern a denial-of-service vector. It arrived in 1.2.31, so the plugin
falls back to plain quoting on the 1.2.25 it still declares support for.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
get_filter_request_var() stores drp_action as an int while the allowlist held
strings, so the strict in_array() rejected every action and each one redirected
without touching the database. The delete also bound $selected_items with its
submitted keys intact, which PDO reads as named parameters, and committed
through Cacti's db_commit_transaction(), which tests a MariaDB-only system
variable and so never commits on MySQL.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The tests that need Cacti to provide db_qstr_rlike() or get_total_row_data()
run in their own process, so defining those functions does not change which
branch the rest of the suite takes.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
thold_process_command_output() dispatches on the topic it is passed, and
'thold' matched none of its branches, so a trigger command run outside the
notification queue recorded neither its exit status nor its output.

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

Also covers the four exit-status and output combinations the command result
logging distinguishes.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Adopts the conventions from Cacti core: tests/bootstrap-unit.php, tests/Helpers
for the stubs, a phpunit.xml carrying error_reporting -1 and
CACTI_TEST_BOOTSTRAP, and composer lint / test / test:coverage scripts so CI
runs the same commands a developer does. The dev toolchain is Cacti's, pinned
to the same platform php 8.1.0.

Cacti core runs Pest and this suite does not, because pest ^2 does not resolve
on PHP 8.1 -- the platform Cacti's own composer.json pins. Releases up to
v2.36.0 conflict with phpunit 10.5.62 and later, every earlier 10.x release is
blocked by advisory PKSA-z3gr-8qht-p93v, and v2.36.1, which does resolve,
requires PHP 8.2. The stack installs on 8.2 and above; 8.1 is the floor this
plugin's CI matrix targets. The tests are written in the plain PHPUnit class
style that Cacti's tests/Pest.php explicitly supports, so they run unchanged
under Pest wherever it is installable.

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

Copy link
Copy Markdown
Member Author

Review follow-up: reverified after the latest rebase. Both TheWitness requests are addressed: there is no plugin-local composer.json, composer.lock, or vendor directory, and the Pest workflow uses Cacti’s Composer toolchain. All eight Copilot findings remain addressed and their threads are resolved. Current CI is rerunning on the updated branch.

@somethingwithproof

Copy link
Copy Markdown
Member Author

Both addressed: there is no composer.json in the plugin, and the unit workflow runs Cacti's own composer test against the checked-out core.

TheWitness pushed a commit that referenced this pull request Aug 18, 2026
…or (#791)

* test: add the PHP 8.1 unit-test harness

Same harness as #773 and #788, with gmp added to the image so the 64-bit
counter arithmetic can be tested exactly.

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

* fix(thold): correct the counter delta and the percent denominator

A previous counter reading of exactly zero was treated as no reading at all,
so the first interval after a device reboot reported a rate of zero. The wrap
modulus was 2^32-1 and 2^64-1 rather than 2^32 and 2^64, losing one count per
wrap, and the 64-bit literal exceeded PHP_INT_MAX so it was parsed as a float
and lost about eleven bits before the subtraction.

The percent-of denominator was cast to int, so a denominator below one
truncated to zero and forced the result to zero, keeping any configured low
threshold in permanent breach.

Refs #785

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

* fix(daemon): store the previous reading in oldvalue, not a timestamp

When a data source produced no sample this cycle the daemon wrote
$currenttime - $rrd_step into oldvalue, so the next poll computed a delta
against a Unix timestamp, took the overflow branch and fabricated a rate in
the billions. The non-daemon path already carries the previous oldvalue
forward; this matches it.

Refs #785

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

* test: follow Cacti's test layout and composer scripts

Adopts the conventions from Cacti core: tests/bootstrap-unit.php, tests/Helpers
for the stubs, a phpunit.xml carrying error_reporting -1 and
CACTI_TEST_BOOTSTRAP, and composer lint / test / test:coverage scripts so CI
runs the same commands a developer does. The dev toolchain is Cacti's, pinned
to the same platform php 8.1.0.

Cacti core runs Pest and this suite does not, because pest ^2 does not resolve
on PHP 8.1 -- the platform Cacti's own composer.json pins. Releases up to
v2.36.0 conflict with phpunit 10.5.62 and later, every earlier 10.x release is
blocked by advisory PKSA-z3gr-8qht-p93v, and v2.36.1, which does resolve,
requires PHP 8.2. The stack installs on 8.2 and above; 8.1 is the floor this
plugin's CI matrix targets. The tests are written in the plain PHPUnit class
style that Cacti's tests/Pest.php explicitly supports, so they run unchanged
under Pest wherever it is installable.

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

* ci: keep plugin PR integration checks on pinned Cacti

* fix(counter): handle non-integer 64-bit readings safely

* ci: bound package index refreshes

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
TheWitness pushed a commit that referenced this pull request Aug 18, 2026
…a source (#790)

* test: add the PHP 8.1 unit-test harness

Same harness as #773 and #788, so whichever lands first the others merge cleanly.

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

* fix(thold): keep a zero reading through tag substitution

thold_str_replace() treated 0 and '0' as absent, so an alert for a value that
had dropped to zero rendered as "Current value is " with a blank, and a
trigger command invoked as --value <CURRENTVALUE> lost the argument and
shifted the ones after it.

Refs #787

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

* fix(thold): return zero when the requested data source is absent

array_search() reports a miss as false, so a guard written against null let it
through and $result['values'][false] read index 0. A lookup for a data source
that does not exist returned the first one's value, which the caller then
compared against the threshold bounds.

Reached today from thold_expression_specialtype_rpn() and the CDEF
substitutions, which pass column names such as upper_limit rather than data
source names. Those call sites still need to read the real column; this only
stops them silently receiving another metric.

Refs #787

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

* test: follow Cacti's test layout and composer scripts

Adopts the conventions from Cacti core: tests/bootstrap-unit.php, tests/Helpers
for the stubs, a phpunit.xml carrying error_reporting -1 and
CACTI_TEST_BOOTSTRAP, and composer lint / test / test:coverage scripts so CI
runs the same commands a developer does. The dev toolchain is Cacti's, pinned
to the same platform php 8.1.0.

Cacti core runs Pest and this suite does not, because pest ^2 does not resolve
on PHP 8.1 -- the platform Cacti's own composer.json pins. Releases up to
v2.36.0 conflict with phpunit 10.5.62 and later, every earlier 10.x release is
blocked by advisory PKSA-z3gr-8qht-p93v, and v2.36.1, which does resolve,
requires PHP 8.2. The stack installs on 8.2 and above; 8.1 is the floor this
plugin's CI matrix targets. The tests are written in the plain PHPUnit class
style that Cacti's tests/Pest.php explicitly supports, so they run unchanged
under Pest wherever it is installable.

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

* ci: keep plugin PR integration checks on pinned Cacti

* test: match the optional Cacti database stub signature

* ci: bound package index refreshes

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@somethingwithproof somethingwithproof changed the title fix(security): consolidated hardening — SQL injection, XSS, unserialize, trigger_cmd security: harden SQL, output escaping, and unserialize paths Aug 18, 2026
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.

5 participants