'192.168.1.55'); check('falls back to REMOTE_ADDR', getRemoteAddr(), '192.168.1.55'); // With no trusted proxies (the default) X-Forwarded-For is client-controlled // and must be ignored, otherwise a client picks the address its hash binds to. $_SERVER = array('REMOTE_ADDR' => '203.0.113.9', 'HTTP_X_FORWARDED_FOR' => '192.168.1.55'); check('ignores XFF when no proxy is trusted', getRemoteAddr(''), '203.0.113.9'); check('config default (undefined) trusts nothing', getRemoteAddr(), '203.0.113.9'); check('ignores XFF from an untrusted peer', getRemoteAddr('10.0.0.1'), '203.0.113.9'); $_SERVER = array('REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '192.168.1.55'); check('uses XFF from a trusted proxy', getRemoteAddr('10.0.0.1'), '192.168.1.55'); // A proxy appends; the left-most values are whatever the client sent. $_SERVER = array('REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, 192.168.1.55'); check('takes the right-most hop, not a spoofed left-most one', getRemoteAddr('10.0.0.1'), '192.168.1.55'); $_SERVER = array('REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, 192.168.1.55 , 10.0.0.9 '); check('skips trusted hops from the right', getRemoteAddr('10.0.0.1, 10.0.0.9'), '192.168.1.55'); check('list may be space separated', getRemoteAddr('10.0.0.1 10.0.0.9'), '192.168.1.55'); $_SERVER = array('REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '10.0.0.9, 10.0.0.1'); check('all hops trusted returns the left-most', getRemoteAddr('10.0.0.1,10.0.0.9'), '10.0.0.9'); $_SERVER = array('REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => ''); check('empty XFF falls back to REMOTE_ADDR', getRemoteAddr('10.0.0.1'), '10.0.0.1'); $_SERVER = array('REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => ' , '); check('XFF of only separators falls back to REMOTE_ADDR', getRemoteAddr('10.0.0.1'), '10.0.0.1'); // ---- zm_session_set_remote_addr(): the address-change handoff ---- // // The session's stored address is refreshed on every request, before any auth // validation runs. Unless the address being replaced is captured at that moment // it is simply lost, and nothing downstream can tell that it just changed - // which is why retaining it belongs here and not at the point of validation. // Request 1 arrives from A; a hash is issued and cached against A. $_SESSION = array(); $_SERVER = array('REMOTE_ADDR' => '192.168.1.55'); zm_session_set_remote_addr(); $_SESSION['AuthHash192.168.1.55'] = 'hash-issued-to-A'; check('session binds to the first address seen', $_SESSION['remoteAddr'], '192.168.1.55'); check('nothing is retained when there is no earlier address', isset($_SESSION['prevRemoteAddr']), false); // Request 2 arrives from B (wifi -> cellular). The hash the client still holds // was issued to A, so A has to survive this refresh to remain checkable. $_SERVER = array('REMOTE_ADDR' => '10.0.0.7'); zm_session_set_remote_addr(); check('session follows the new address', $_SESSION['remoteAddr'], '10.0.0.7'); check('the displaced address is retained', $_SESSION['prevRemoteAddr'], '192.168.1.55'); check('the retention is timestamped', isset($_SESSION['prevRemoteAddrAt']), true); check('the hash cached against the old address is still reachable', isset($_SESSION['AuthHash192.168.1.55']), true); // An unchanged address must not displace what we are still holding. zm_session_set_remote_addr(); check('a repeat request does not overwrite the retained address', $_SESSION['prevRemoteAddr'], '192.168.1.55'); // Request 3 arrives from C before a hash was ever issued to B. Only one previous // address is kept, and A's now-unreachable slot is dropped rather than left. $_SERVER = array('REMOTE_ADDR' => '172.16.0.3'); zm_session_set_remote_addr(); check('only one previous address is retained', $_SESSION['prevRemoteAddr'], '10.0.0.7'); check('its cached hash is discarded rather than accumulating', isset($_SESSION['AuthHash192.168.1.55']), false); // A login is a privilege boundary: nothing from before it stays acceptable. $_SESSION['prevRemoteAddr'] = '10.0.0.7'; $_SESSION['prevRemoteAddrAt'] = time(); // Silenced: is_session_started() always reports false under CLI (by design, see // session.php), so this re-enters session_start(). The assertions below still // cover the behaviour we care about. @zm_session_regenerate_id_login(); check('login drops any retained earlier address', isset($_SESSION['prevRemoteAddr']), false); check('login drops its timestamp too', isset($_SESSION['prevRemoteAddrAt']), false); check('login binds to the address it came from', $_SESSION['remoteAddr'], '172.16.0.3'); echo "\n$passes passed, $failures failed\n"; ob_end_flush(); exit($failures ? 1 : 0); } // end global namespace block ?>