With ZM_AUTH_HASH_IPS on, the client address bound into the auth hash was
taken from the left-most X-Forwarded-For value whenever the header was
present, both when PHP generated the hash (getRemoteAddr()) and when PHP or
zms validated it (getAuthUser(), zmLoadAuthUser()). The header is client
controlled, so anyone holding a leaked hash could replay it from anywhere by
sending the address it was bound to.
Add ZM_AUTH_TRUSTED_PROXIES, a list of exact reverse proxy addresses.
X-Forwarded-For is now used only when REMOTE_ADDR is one of them, and is read
from the right, skipping hops that are themselves listed proxies, so values a
client prepends are never chosen. With the option empty, the default, the
header is ignored and REMOTE_ADDR is used.
PHP (web/includes/Network.php getRemoteAddr()) and C++ (ClientAddress() in
zm_utils, used by zmLoadAuthUser()) implement the same rule so generation and
validation continue to agree. Every PHP caller already routes through
getRemoteAddr(), so session.php and auth.php need no change.
Reverse proxy users who enable ZM_AUTH_HASH_IPS must list their proxy in the
new option; until they do, hashes bind to the proxy address, which still
validates but no longer distinguishes clients. This is the behaviour change
that the #4921 work avoided by trusting the header.
The option is added through ConfigData only, like other recent options;
zmupdate.pl --freshen inserts it, zms falls back to the compiled-in default and
PHP treats an undefined constant as empty, so no schema migration is needed.
Tests: ClientAddress Catch2 case; tests/php/test_remote_addr.php updated for
the trusted-proxy rule.
refs GHSA-72rf-54rm-798c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit acea5889dec0826f595cb736147a5fdc9c94a2a9)
The Options field was a single line text input, so a list of options had to
be typed as one comma separated run. Make it a textarea, sized to the number
of entries it already holds, so each option can go on its own line.
Nothing needs to parse this: both consumers already take a set of separator
characters, they were just never given the newlines.
av_dict_parse_string() (Ffmpeg) and Split() (Libvlc) both skip empty
entries, so a blank line, crlf, or a trailing newline all work, and comma
separated values keep working unchanged.
The separator set is a named constant rather than a literal at each call
site so the tests exercise the value the cameras actually pass - with a
literal they would keep passing if a call site were reverted. Verified by
setting it back to "," which fails both test cases.
RemoteCameraHTTP::GetResponse()'s non-PCRE parser (the default when built
without PCRE) copied server-controlled HTTP response header values into
fixed-size static buffers with unbounded strcpy()/sprintf(), and with
strncpy() bounds derived from delimiters rather than the buffer size. A
malicious or MitM'd HTTP camera could overflow status_mesg[256],
connection_type[32], content_type[32] and content_boundary[64],
corrupting adjacent parser state (content_length, content_boundary_len)
for DoS and secondary heap corruption.
Add zm_strncpy(): a bounded copy that always null-terminates (strcpy
overflows; strncpy skips the terminator on truncation) and optionally
caps to a field length n for delimiter-bounded values whose source is
not null-terminated at the field end. Route every header copy in
GetResponse() through it. The content_boundary "--" prefix is written
separately and content_boundary_len is taken from strlen() of the
resulting (now bounded) string so the compare length at the multipart
subheader match stays correct.
The two subcontent-header strncpy() calls were already bounded to
sizeof-1 with an explicit terminator and are left as-is.
Adds a tests/zm_utils.cpp self-check covering fit, truncation,
delimiter-limit, delimiter-over-buffer, and empty-source cases.
Refs GHSA-93j4-rcp9-9jx6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>