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 <noreply@anthropic.com>
This commit is contained in:
Isaac ConnorandClaude Opus 5.5 committed 2026-10-15 22:27:19 -04:00
1 parent 68298351ab
commit 4d7609d36e
3 files changed
+105 -3

No files matched your search

+36 -3
View File
@@ -27,6 +27,7 @@
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <cctype>
#include <fstream>
#include <glob.h>
#include <string>
@@ -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<char>(tolower(static_cast<unsigned char>(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;
+11
View File
@@ -23,6 +23,7 @@
#include "config.h"
#include "zm_config_data.h"
#include "zm_config_defines.h"
#include <map>
#include <string>
#include <unordered_map>
@@ -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<std::string, std::string> 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;
+58
View File
@@ -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();
}