From 4d7609d36ee0a0330ff792db8b2f0d64bd26689b Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 15 Oct 2026 22:22:25 -0400 Subject: [PATCH] fix: let options in zm.conf and conf.d win over the database in the daemons zm.conf and conf.d are set by the owner of the box; the Config table by the ZoneMinder admin through the Options page. The owner has to be able to limit the admin, so a file setting must win. PHP and Perl already let it. process_configfile() kept only a fixed list of names (database credentials, paths, server identity, stream socket) and dropped the rest, so zmc, zms and zmu read every other option from the database alone. So with ZM_OPT_USE_AUTH=1 in conf.d and 0 in Options, the web UI asked for a login while nph-zms served streams and snapshots to anyone. The same split applied to every option, ZM_AUTH_RELAY and ZM_AUTH_HASH_IPS among them. process_configfile() now records every name = value in StaticConfig::file_options, later files winning, and zmLoadStaticConfig() clears it first so a reload forgets a removed line. Config::Load() applies those values over the database ones, also when the database can't be read. A boolean may be written yes/no, true/false or on/off as well as 1/0 (ConfigFileValue), since the Config table's 1/0 convention is not what people type into a file. Overrides are logged by name only, as the value may be a secret. Tested live on 1.38: with ZM_OPT_USE_AUTH=1 in conf.d and 0 in the database, an unauthenticated nph-zms snapshot request answered 200 with an image before and 403 after. Without the override it still answers 200. refs #5289 Co-Authored-By: Claude Opus 5.5 --- src/zm_config.cpp | 39 +++++++++++++++++++++++++++--- src/zm_config.h | 11 +++++++++ tests/zm_config.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/zm_config.cpp b/src/zm_config.cpp index 680aa2cfd..11f71a4e4 100644 --- a/src/zm_config.cpp +++ b/src/zm_config.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ // config. void zmLoadStaticConfig() { + // Start over, so that a reload forgets a line that has been removed. + staticConfig.file_options.clear(); // Process name, value pairs from the main config file first process_configfile(ZM_CONFIG); @@ -190,6 +193,8 @@ void process_configfile(char const *configFile) { white_len += strspn(val_ptr, "\'\""); val_ptr += white_len; + staticConfig.file_options[name_ptr] = val_ptr; + if ( strcasecmp(name_ptr, "ZM_DB_HOST") == 0 ) staticConfig.DB_HOST = std::string(val_ptr); else if ( strcasecmp(name_ptr, "ZM_DB_NAME") == 0 ) @@ -249,9 +254,7 @@ void process_configfile(char const *configFile) { staticConfig.STREAM_SOCKET_STALL_SECS = parse_unsigned_setting(name_ptr, val_ptr, staticConfig.STREAM_SOCKET_STALL_SECS); else { - // We ignore this now as there may be more parameters than the - // c/c++ binaries are bothered about - // Warning( "Invalid parameter '%s' in %s", name_ptr, ZM_CONFIG ); + // Any other option is in file_options, which Config::Load() applies. } } // end foreach line of the config } @@ -394,6 +397,7 @@ void Config::Load() { " OR `DefaultValue` IS NULL"); if (!result) { Warning("Failed to load config from database, using compiled-in defaults"); + ApplyFileOptions(); return; } @@ -410,6 +414,35 @@ void Config::Load() { Debug(1, "Config loaded: %d items overriding compiled-in defaults (%zu registered)", loaded, bindings_.size()); + ApplyFileOptions(); +} + +std::string ConfigFileValue(const std::string &value, const char *type) { + if (strcmp(type, "boolean") != 0) return value; + // The Config table stores 1/0; a person editing a file may write a word. + std::string word; + for (char c : value) word += static_cast(tolower(static_cast(c))); + if (word == "yes" or word == "true" or word == "on") return "1"; + if (word == "no" or word == "false" or word == "off") return "0"; + return value; +} + +void Config::ApplyFileOptions() { + for (const auto &option : staticConfig.file_options) { + auto bind_it = bindings_.find(option.first); + if (bind_it == bindings_.end()) continue; // a static setting, or not one zmc uses + + const char *type = "string"; + switch (bind_it->second.type) { + case MemberBinding::BOOL: type = "boolean"; break; + case MemberBinding::INT: type = "integer"; break; + case MemberBinding::DOUBLE: type = "decimal"; break; + case MemberBinding::STRING: break; + } + // Name only: the value may be a secret such as ZM_AUTH_HASH_SECRET. + Debug(1, "%s is set in a config file, overriding the database", option.first.c_str()); + ApplyItem(option.first.c_str(), ConfigFileValue(option.second, type).c_str(), type); + } } Config config; diff --git a/src/zm_config.h b/src/zm_config.h index a30f6e450..34955ee10 100644 --- a/src/zm_config.h +++ b/src/zm_config.h @@ -23,6 +23,7 @@ #include "config.h" #include "zm_config_data.h" #include "zm_config_defines.h" +#include #include #include @@ -73,6 +74,10 @@ struct StaticConfig { unsigned int STREAM_SOCKET_QUEUE_BYTES = 8 * 1024 * 1024; unsigned int STREAM_SOCKET_QUEUE_MSGS = 256; unsigned int STREAM_SOCKET_STALL_SECS = 10; + // Every name = value in zm.conf and conf.d, later files winning. The owner of + // the box sets these and the ZoneMinder admin the Config table, so + // Config::Load() applies them over the database values. + std::map file_options; std::string capture_file_format; std::string analyse_file_format; std::string general_file_format; @@ -81,6 +86,10 @@ struct StaticConfig { extern StaticConfig staticConfig; +// A value from zm.conf or conf.d as the Config table would hold it for an +// option of `type`: a boolean may be written yes/no, true/false or on/off. +std::string ConfigFileValue(const std::string &value, const char *type); + class ConfigItem { private: std::string name_; @@ -127,6 +136,8 @@ class Config { ~Config() = default; void Load(); + // Applies StaticConfig::file_options to the options they name. + void ApplyFileOptions(); }; extern Config config; diff --git a/tests/zm_config.cpp b/tests/zm_config.cpp index 13e35a036..4421e2712 100644 --- a/tests/zm_config.cpp +++ b/tests/zm_config.cpp @@ -130,3 +130,61 @@ TEST_CASE("Config members hold compiled-in defaults before any database load") { REQUIRE(fresh.font_file_location != nullptr); REQUIRE(std::string(fresh.font_file_location) != ""); } + +// zm.conf and conf.d belong to the owner of the box, the Config table to the +// ZoneMinder admin, and the owner must be able to limit the admin. PHP and Perl +// already let a file value win; the daemons ignored every file option outside +// a fixed list and read it from the database, so an admin could turn off, say, +// ZM_OPT_USE_AUTH for zms while the web UI kept it on. +TEST_CASE("Config: an option set in a config file wins over the database") { + std::string first = WriteTempConf("ZM_OPT_USE_AUTH=0\nZM_AUTH_HASH_TTL=5\n"); + std::string second = WriteTempConf("ZM_OPT_USE_AUTH=1\nZM_NOT_AN_OPTION=x\n"); + staticConfig.file_options.clear(); + process_configfile(first.c_str()); + process_configfile(second.c_str()); + unlink(first.c_str()); + unlink(second.c_str()); + + // conf.d files are read in order and a later one wins. + REQUIRE(staticConfig.file_options["ZM_OPT_USE_AUTH"] == "1"); + + Config cfg; + cfg.opt_use_auth = false; // as loaded from the Config table + cfg.auth_hash_ttl = 2; + cfg.ApplyFileOptions(); + REQUIRE(cfg.opt_use_auth == true); + REQUIRE(cfg.auth_hash_ttl == 5); + staticConfig.file_options.clear(); +} + +TEST_CASE("Config: a boolean in a config file may be written as a word") { + // The Config table stores 1/0, but a person editing a file writes yes or true. + const char *on[] = {"1", "yes", "Yes", "true", "on"}; + const char *off[] = {"0", "no", "false", "off", ""}; + for (const char *value : on) { + CAPTURE(value); + staticConfig.file_options = {{"ZM_OPT_USE_AUTH", value}}; + Config cfg; + cfg.ApplyFileOptions(); + REQUIRE(cfg.opt_use_auth == true); + } + for (const char *value : off) { + CAPTURE(value); + staticConfig.file_options = {{"ZM_OPT_USE_AUTH", value}}; + Config cfg; + cfg.opt_use_auth = true; + cfg.ApplyFileOptions(); + REQUIRE(cfg.opt_use_auth == false); + } + staticConfig.file_options.clear(); +} + +TEST_CASE("Config: static settings in a config file still apply") { + std::string path = WriteTempConf("ZM_SERVER_NAME=box1\n"); + staticConfig.file_options.clear(); + staticConfig.SERVER_NAME.clear(); + process_configfile(path.c_str()); + unlink(path.c_str()); + REQUIRE(staticConfig.SERVER_NAME == "box1"); + staticConfig.file_options.clear(); +}