Files
Isaac ConnorandClaude Opus 5 935d0cf385 fix: keep an auth hash valid when the client address changes refs #4921
ZM_AUTH_HASH_IPS binds the auth hash to the client address. When that address
changes mid-session - a phone moving between wifi and cellular is the common
case - the hash the browser is still holding no longer matches the address we
now see, and the user is bounced to the login page. The usual workaround is to
turn ZM_AUTH_HASH_IPS off entirely.

Accept the address the request arrives from plus the one it arrived from
immediately before, so an in-flight hash validates once and generateAuthHash()
then reissues against the new address. Addresses are matched exactly. A netmask
was considered and rejected: accepting a whole subnet would let any other host
on the client's network replay a stolen hash, which on a home LAN includes the
cameras themselves.

The previous address is only accepted for as long as a hash issued to it would
itself still be valid (ZM_AUTH_HASH_TTL), so this widens which address is
accepted without extending how long any hash lives. A login clears it, since
nothing from before a privilege boundary should stay acceptable, and only one
previous address is ever retained.

userFromSession() needed the same treatment: it looks the cached hash up by the
live address, so after a change the slot does not exist yet and the user was
reported as not logged in regardless of what getAuthUser() would have accepted.

Also centralises the X-Forwarded-For/REMOTE_ADDR handling in getRemoteAddr(),
replacing four duplicated copies across session.php and auth.php. Those copies
sat on both the generation and validation sides, so any drift between them broke
authentication outright behind a reverse proxy. Network.php holds only that
address parsing; which addresses an auth hash is accepted from is auth policy
and lives in auth.php.

This is web-side only; zms has no session, so a stream request still fails once
on an address change and recovers through the existing auth-refresh path in
MonitorStream.js.

Tests: tests/php/test_remote_addr.php covers getRemoteAddr() parsing and the
session address rotation, and needs no config or database - 18 assertions, all
pass. tests/php/test_auth_hash_candidate_addrs.php covers the acceptance window
including both sides of the TTL boundary; it bootstraps config.php as the other
tests in that directory do and so needs an installed tree to run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01477mR97vfnK6zczbHgzq6T
2026-08-17 20:52:10 -04:00

27 lines
1.1 KiB
PHP

<?php
//
// ZoneMinder network address helpers
//
// Kept dependency-free so it can be included from anywhere - session.php pulls
// it in before the rest of the web bootstrap exists - and unit tested
// standalone. Policy decisions that happen to involve addresses do not belong
// here; the auth hash's use of getRemoteAddr() lives in auth.php.
//
// Return the effective client address: the first hop of X-Forwarded-For when
// present (reverse-proxy setups), otherwise REMOTE_ADDR. Only the first value is
// used to avoid trusting spoofed multi-value headers.
//
// Note that X-Forwarded-For is taken on trust; there is no trusted-proxy list.
// A client connecting directly can therefore choose the address it is bound to.
// That is long-standing behaviour, kept here so generation and validation agree,
// but it means ZM_AUTH_HASH_IPS is only meaningful when a proxy you control
// overwrites the header, or when nothing is proxied at all.
function getRemoteAddr() {
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
}
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
}
?>