Commit Graph
128 Commits
Author SHA1 Message Date
Isaac ConnorandClaude Opus 5 b9f565854f refactor: stop authenticating the request when auth.php is included
auth.php ended in a 130-line block at file scope, so merely including the file
authenticated the current request: it read $_REQUEST, opened a session, queried
the database, and on a login could rewrite the user's stored password hash and
populate $_SESSION. Any caller that wanted one of the functions in the file got
all of that as a side effect, and the order of includes decided when it ran.
HostController requires auth.php twice purely to reach generateAuthHash() and
validateToken().

Move the block into zm_authenticate_request() and call it explicitly from the
two places that want it, web/index.php and AppController::beforeFilter(). The
function returns the ZM\User or null and still sets the global $user, so the
views, ajax handlers and API controllers that read that global are unaffected.
HostController now gets only the function definitions from its requires, which
is all it ever wanted.

Inside a function the five `unset($user)` calls would drop the local binding
and leave the global set, so they become `$user = null` - the idiom the rest of
the file already uses for this, and one that keeps isset($user) false for the
gate at index.php:255. The block's other locals ($ret, $username, $password,
$sql) no longer leak into the caller's scope, which in beforeFilter() means they
can no longer collide with the variables of the same name it assigns just after.

The body is otherwise unchanged; `git diff -w` shows only the wrapper, those
five assignments and the return.

Tests: tests/php/test_auth_no_include_side_effects.php tokenises auth.php and
asserts nothing executes at file scope, with a fixture check so a broken
detector cannot pass vacuously. 4 assertions, all pass. Verified it reports the
pre-refactor file's file-scope block, so it would have caught this.

Not covered by tests: the login, logout, auth-hash and API token flows this
touches. auth.php cannot be included without a database (User.php pulls in
database.php, which connects at include time), so the check is structural.
Needs manual testing on an installed tree before merging.

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
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
Isaac ConnorandClaude Opus 4.8 6b94903e63 fix: prevent auth bypass when token validation fails
validateToken() returns array(false, $errorMessage) when a token is
invalid or its signature fails. The token branch in auth.php assigned
$user = $ret[0] unconditionally, leaving $user as boolean false on
failure. Because isset($false) is true in PHP, the ZM_OPT_USE_AUTH gate
in index.php (!isset($user)) was skipped, allowing unauthenticated
access via any malformed ?token= value. It also permitted an
unauthenticated DoS: downstream $user->Username() on a bool fatals.

Check !$ret[0] and unset($user) on failure, mirroring the existing
validateUser branch, so $user stays undefined and the auth gate blocks
the request. The API call sites already throw UnauthorizedException on
!$user and are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 01:20:16 -04:00
Isaac ConnorandClaude Opus 4.8 bac9b8af01 fix: stop IP-less auth hash from poisoning the IP-bound cache slot refs #4921
generateAuthHash() cached every hash under the IP-keyed slot
'AuthHash'.$remoteAddr, but the value was IP-bound only when $useRemoteAddr was
set. getZmuCommand() calls generateAuthHash(false, true), which wrote an
IP-less hash into the IP-bound slot and reset AuthHashGeneratedAt. Because the
status poll runs getZmuCommand (web/ajax/status.php) right before emitting the
auth hash, the poll then served the IP-less hash to the browser; the next
IP-bound request was rejected by the validator, redirecting the user to login
roughly every poll. This happens with a completely stable client IP, so it is
distinct from the IP-rotation case.

Key the cache slot by the address actually baked into the value: only use the
session address when this caller asked for it (and AUTH_HASH_IPS is on). IP-less
and IP-bound hashes now occupy separate slots and can no longer clobber each
other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:07:44 -04:00
SteveGilvarryandClaude Opus 4.8 5ae582b1a1 fix: emit a single session cookie on login refs #2471
On successful login auth.php called zm_session_clear() followed by
zm_session_regenerate_id(), which together emitted three Set-Cookie
ZMSESSID headers: a deletion, a throwaway intermediate id, and the final
authenticated id. This is the multiple-cookie behaviour reported in #2471.

Add zm_session_regenerate_id_login(), which clears the pre-auth session
data and calls session_regenerate_id(true) to issue one new id while
deleting the old session server-side. Same anti-session-fixation
guarantee in a single Set-Cookie. Logout (zm_session_clear) and the
periodic mid-session regeneration are left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 20:25:49 +10:00
Isaac Connor 671e1c361f fix: stop warning on URL/session user mismatch in getAuthUser
The auth hash is stateless (HMAC over secret+username+password+IP+date)
and intentionally independent of the PHP session. Stream/image URLs
minted while user A was logged in keep working for user A's hash even
after the active session has rotated to B (re-login, session timeout +
new login, another tab, etc.), which is the design.

The eager Warning was firing on these legitimate cross-session requests
and producing log noise. A genuinely tampered request without a valid
hash still falls through to the existing failure-path Info log, which
now also reports sessionUser for diagnostics.
2026-05-15 23:01:00 -04:00
Isaac ConnorandClaude Opus 4.7 fbf73de262 fix: align auth hash validation with generation and warn on user mismatch
- AppController.php: stop overwriting $_SESSION['remoteAddr'] with bare
  REMOTE_ADDR right after zm_session_start() already populated it from
  HTTP_X_FORWARDED_FOR. The clobber bound generated hashes to the proxy
  IP, but getAuthUser() validates against XFF, so any hash produced
  inside the legacy stateful API path was DOA behind a reverse proxy.
- getAuthUser(): prefer the URL user= parameter over
  \$_SESSION['username'] for filtering, matching what zms's
  zmLoadAuthUser does, and honor ZM_CASE_INSENSITIVE_USERNAMES on the
  primary filter. Warn when the URL user= disagrees with the session
  username (stale hash, cross-tab contamination, or tampered request).
- Add a Debug input dump on entry and an Info-level failure line that
  reports filterUser, XFF, REMOTE_ADDR, rowsTried and the TTL window so
  the next 401 surfaces which input is wrong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:46:01 -04:00
copilot-swe-agent[bot]andconnortechnology 5b51d086e1 fix: use HTTP_X_FORWARDED_FOR in auth hash validation to fix AUTH_HASH_IPS with reverse proxy
When AUTH_HASH_IPS is enabled and ZoneMinder is behind a reverse proxy
(e.g. Nginx in front of Apache), the hash is generated using
HTTP_X_FORWARDED_FOR (the real client IP) but was validated using only
REMOTE_ADDR (the proxy's IP), causing all authentication to fail.

Fix by consistently using HTTP_X_FORWARDED_FOR (first IP only, to guard
against spoofed multi-value headers) with REMOTE_ADDR as fallback in
all three places:
- web/includes/session.php: where remoteAddr is stored for hash generation
- web/includes/auth.php: getAuthUser() validation (PHP, also used by zms CGI)
- src/zm_user.cpp: zmLoadAuthUser() validation (C++ zms binary)

refs #4758

Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/959dfe9d-edea-4de5-a3a0-f90b758e5628

Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
2026-05-04 14:53:38 +00:00
Isaac ConnorandClaude Opus 4.6 5561829450 fix: include username in auth relay and fix stale auth in stream restart
- Add user= parameter to get_auth_relay() so zms can use the indexed
  Username column instead of iterating all users to validate the hash
- Apply the same fix to Event.php getStreamSrc() and getThumbnailSrc()
- Tighten Monitor.php from isset() to !empty() for consistency
- In MonitorStream.js start(), check if the auth hash in the img src
  matches the current auth_hash before resuming via CMD_PLAY. If stale,
  fall through to rebuild the URL with fresh auth_relay. This prevents
  long-running montage pages from spawning zms with expired credentials.
- Downgrade zms auth failure from Error to Warning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 10:01:47 -04:00
Isaac ConnorandClaude Opus 4.6 3432e39c45 feat: add Remember Me checkbox to login page
Add ZM_OPT_USE_REMEMBER_ME config option (auth section, requires
ZM_OPT_USE_AUTH) that controls whether a Remember Me checkbox appears
on the login form. When enabled and unchecked, the session cookie
lifetime is set to 0 so the browser discards it on close, logging the
user out. When checked, the session persists for ZM_COOKIE_LIFETIME.
When the option is disabled, behavior is unchanged.

- ConfigData.pm.in: new ZM_OPT_USE_REMEMBER_ME boolean option
- login.php: checkbox between password field and reCAPTCHA/submit
- session.php: use lifetime=0 when remember me is off
- actions/login.php: set/clear ZM_REMEMBER_ME cookie on login, also
  update $_COOKIE so zm_session_start sees it in the same request
- auth.php: clear ZM_REMEMBER_ME cookie on logout
- en_gb.php: add RememberMe translation string

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:41:25 -05:00
Isaac Connor c2ab47ae0d Don't audit log logins 2026-02-23 22:36:03 -05:00
Isaac ConnorandClaude Opus 4.6 e6ace6fcf4 feat: add AUDIT logging level for tracking administrative changes
Add a new AUDIT logging level (-5) between PANIC (-4) and NOLOG (shifted
to -6) across C++, PHP, and Perl loggers. AUDIT entries use code 'AUD'
and syslog priority LOG_NOTICE. They record who changed what, from where,
for monitors, filters, users, config, roles, groups, zones, states,
servers, storage, events, snapshots, control caps, and login/logout.

AUDIT entries have their own retention period (ZM_LOG_AUDIT_DATABASE_LIMIT,
default 1 year) separate from regular log pruning. The log pruning in
zmstats.pl and zmaudit.pl now excludes AUDIT rows from regular pruning
and prunes them independently.

Critical safety: the C++ termination logic is changed from
'if (level <= FATAL)' to 'if (level == FATAL || level == PANIC)' to
prevent AUDIT-level log calls from killing the process.

Includes db migration zm_update-1.39.1.sql to shift any stored NOLOG
config values from -5 to -6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:19:20 -05:00
Isaac ConnorandClaude Opus 4.5 60fbea3880 fix: security and code quality improvements in auth.php
- Fix SQL injection vulnerability in migrateHash() by using prepared statements
- Add null/empty check in password_type() to prevent array access error
- Remove dead code branch in generateAuthHash() (unreachable $_SESSION check)
- Fix PHP version in error message (5.3 -> 5.5 for password_hash)
- Prevent username enumeration by using consistent error messages
- Fix spacing inconsistency in substr() call
- Add TODO comment about MD5 hash weakness

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 15:10:55 -05:00
Isaac ConnorandClaude Opus 4.5 4e60cb96a7 feat: add User Roles feature for reusable permission templates
Add a User Roles system where roles define reusable permission templates.
When a user has a role assigned, the role provides fallback permissions
(user's direct permissions take precedence; role is used when user has 'None').

Database changes:
- Add User_Roles table with same permission fields as Users
- Add Role_Groups_Permissions table for per-role group overrides
- Add Role_Monitors_Permissions table for per-role monitor overrides
- Add RoleId foreign key to Users table

Permission resolution order:
1. User's direct Monitor/Group permissions (if not 'Inherit')
2. Role's Monitor/Group permissions (if user has role)
3. Role's base permission (if user's is 'None')
4. User's base permission (fallback)

Includes:
- PHP models: User_Role, Role_Group_Permission, Role_Monitor_Permission
- Role management UI in Options > Roles tab
- Role selector in user edit form
- REST API endpoints for roles CRUD
- Translation strings for en_gb

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 13:34:27 -05:00
Isaac Connor 50fbbb0154 When logging in by auth hash, clear old auth in session. 2026-01-04 18:27:14 -05:00
Isaac Connor 3957a212f8 Dont error when using auth_relay=none and there is no username in session 2025-11-10 13:47:50 -05:00
Isaac Connor b1e7266319 Turn off debug 2025-09-06 11:38:50 -04:00
Isaac Connor ca8a3048ed Turn off debug 2025-09-06 11:38:40 -04:00
Isaac Connor b5e1d5e611 Fix invalid canEditMonitor call, simplify login in canView and canEdit to prefer per-monitor permissions 2024-03-05 14:02:01 -05:00
Isaac Connor 31af9d905c Still generate an auth if no password present. 2024-02-08 13:31:21 -05:00
Isaac Connor 0d6e7bc082 Add debugging to editableMonitor and handle when permission is set to Create. 2024-01-21 11:28:34 -05:00
Isaac Connor 31036ae9dd Users with Create permission should be able to view console 2024-01-03 16:41:56 -05:00
Isaac Connor 2d49e93405 Introduce a Create permission for Monitors, so that a given user may Edit existing monitors, but not create new ones. 2024-01-02 12:18:09 -05:00
Isaac Connor 9f04d90a27 When ZM_AUTH_HASH_IPS is off, don't use remote ip in storing auth hash in session. If ips are constantly changing it breaks. 2023-11-13 11:31:45 -05:00
Isaac Connor c6a7fe949a Use different syntax for property access 2023-09-13 13:10:01 -04:00
Isaac Connor 8748d6a1cc Handle unset _SESSION['remoteAddr'] 2023-08-16 15:23:34 -04:00
Isaac Connor 91b4e299b9 Demote failed login to Warn instead of Error 2023-08-16 10:37:06 -04:00
Isaac Connor 8f9a4dc825 Use User object to get Monitor_Permission and Group_Permissions hoping it will be cached there. 2023-08-02 18:07:35 -04:00
Isaac Connor 81b9ac5efd Ignore inherit on monitor permission. We were returning true instead 2023-06-07 16:00:27 -04:00
Isaac Connor 00113208bf fix typo 2023-04-28 08:44:47 -04:00
Isaac Connor c87316d3a1 Handle not being logged in 2023-04-24 10:13:45 -04:00
Isaac Connor 969baa3891 Convert user from an array to a User object 2023-04-23 12:57:29 -04:00
Isaac Connor d5baadca84 Comment out debug 2023-04-02 16:10:24 -04:00
Isaac Connor 6c8a2ad05a add support for shortened user= pass= which is used in auth_relay=PLAIN 2023-03-22 08:06:02 -04:00
Isaac Connor 97879eab49 Fix missing s causing problems when Group permission set to None 2023-03-08 10:59:33 -05:00
Isaac Connor 2e58ea1a42 return false if not logged in in canView. defaultUser is global, need to tell it so. 2023-02-27 17:01:58 -05:00
Isaac Connor 9ceb22db8f Fix auth for monitor_permissions... 2023-02-25 16:00:34 -05:00
Isaac Connor bf810ec8c4 test for existence of username in session to prevent error outputs 2023-02-20 10:20:59 -05:00
Isaac Connor 1a8a84da96 Remove debug when relay not hashed 2023-02-15 15:02:22 -05:00
Isaac Connor 40e7f8a517 Handle SESSION[password] not being set which happens when you switch AUTH_RELAY to plain. 2023-02-14 17:10:58 -05:00
Isaac Connor 97e464b342 Comment out debugging 2022-11-15 14:17:30 -05:00
Isaac Connor 3c36233dd8 Put in all the new logic for visibleMonitor and editableMonitor referencing Monitor_Permissions and Group_Permissions 2022-11-02 13:25:53 -04:00
Isaac Connor 2ee142813e start to rough in an editable monitor function 2022-11-02 12:37:47 -04:00
Isaac Connor 5734e38f52 Add group permission checking to visibleMonitor 2022-11-02 12:36:42 -04:00
Isaac Connor d00536915e Move getting time into the scope where it is used 2022-11-02 10:07:01 -04:00
Isaac Connor 5c8343f737 remoteAddr is set when session is opened. So no need to do it in auth. session_clear closes the session so no need to close it first 2022-10-17 17:23:45 -04:00
Isaac Connor 2329dda323 Merge pull request #3609 from connortechnology/fix_non_post_auth
Move relevant code out of includes/actions/auth.php into includs/auth…
2022-10-14 14:09:09 -04:00
Isaac Connor 9fc0d26d32 If we don't have session, we can still generate an auth hash. We kinda need this for zmu calls from API 2022-09-28 18:35:59 -04:00
megasaturnv 8fb6477820 Case insensitive login - Fix spacing ZoneMinder/zoneminder#3516 2022-09-21 16:47:16 +01:00
Isaac Connor b4fc5abe95 Failure to auth by hash is common, the end result is a redirect to login page. Change the log level to INF 2022-09-13 17:59:53 -04:00