Two ways a System=View account could read secrets through the Config API.
The oracle: ConfigsController::index() passed the caller's named filter to
buildFilter() and returned the matched rows. redactSecret() blanked a secret
Value in the response but not in the WHERE clause, so a filter such as
Name:ZM_AUTH_HASH_SECRET/Value LIKE:a% returned the row for a correct prefix
and nothing otherwise - the row count leaked the secret a character at a time,
and that secret signs auth tokens. index() now detects a filter on the Value
column and drops the secret rows from the result, so their presence reveals
nothing; a filter on any other column is unaffected.
The flags: ZM_JANUS_SECRET, ZM_UPLOAD_PASS and ZM_MQTT_PASSWORD held secrets
but were not marked Private, so the API returned them to System=View and
skin.js.php wrote them into every page as JavaScript constants. Mark them
private in ConfigData.pm.in (fresh installs) and add a Config UPDATE to the
untagged 1.39.41 migration (existing installs). No web page reads the three;
zmfilter, zmc and the Janus integration read them server side.
Tested: tests/php/test_config_value_filter.php (11 cases) covers the Value
detection, including Model.Value, case, and the DefaultValue non-match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Any enabled, API-enabled account could read the effective configuration,
including secrets, whatever its permissions. Confirmed on a live instance: a
user with every area set to None retrieved ZM_DB_PASS from
/api/configs/viewByName/ZM_DB_PASS.json.
The controller already required System Edit to change a config but checked
nothing at all to read one, so index, view, viewByName and categories were open
to anyone the API let in. They now require the same System permission the web
ui's Options page requires.
Permission alone is not enough, because nothing outside the server has any use
for these values. Two kinds are now withheld from every caller, administrators
included:
- Rows flagged Private in the Config table. That flag already existed and was
loaded into $zm_config, but nothing ever acted on it. It covers
ZM_AUTH_HASH_SECRET, which is enough to forge an authentication hash for any
user, and the reCaptcha secret.
- The database credentials. These are read from zm.conf and conf.d rather than
the table, so they have no row to flag and are listed by name. This is also
why filtering the table alone would not have been enough: index appends the
file-backed values to its response.
Requesting an unknown name logged print_r($zm_config, true), copying the whole
effective configuration into the log and anything collecting it. It now logs the
name at Debug.
Verified against a live instance, before and after, with a temporary System=None
API user: reading ZM_DB_PASS returned the password and now returns 401; an
administrator gets an empty Value for ZM_DB_PASS and ZM_AUTH_HASH_SECRET, the
293-entry index carries no secret values, and ordinary settings such as
ZM_WEB_TITLE still read normally.
The config-api RCE fix (b036408a5) guards edit() and delete() with
($user['System'] == 'Edit'), but $user is a ZM\User object that does
not implement ArrayAccess and whose System property is protected. Array
access on it raises "Error: Cannot use object of type ZM\User as array",
so the endpoint fatals with HTTP 500 for every authenticated user. This
blocks the RCE only by accident and also breaks config editing for
legitimate System=Edit admins.
Use $user->System(), matching the idiom in every other API controller
(States, Servers, Monitors, Users, etc.).
Refs GHSA-mvj8-mqqq-2w5f.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add RBAC checks to ConfigsController edit() and delete() requiring
System=Edit permission, matching the pattern used by other controllers.
Harden System/Readonly column checks with !empty() to handle missing
columns gracefully. Fix command injection in Event.php by using
ZM_PATH_FFMPEG constant with escapeshellarg() instead of hardcoded
unsanitized ffmpeg call. Add is_executable() validation at all exec()
sites using ZM_PATH_FFMPEG as defense-in-depth against poisoned config
values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>