What's Changed
This release hardens four correctness and security gaps identified in the post-v2.0.5 audit. No externally visible authentication behavior is altered.
Security Fixes
get_client_ip() — IPv4-mapped IPv6 normalization
Dual-stack PHP-FPM and Apache setups can present REMOTE_ADDR as ::ffff:192.168.1.1 (RFC 4291 IPv4-mapped IPv6). These addresses passed filter_var() as IPv6 and therefore never matched IPv4 CIDR bypass rules, incorrectly forcing Duo on bypass-listed users. The fix normalizes ::ffff:<dotted-quad> to plain IPv4 after all header resolution, ensuring all three bypass tiers evaluate the correct address family.
ip_in_cidr() — CIDR prefix length bounds checks
No validation was performed on the prefix length extracted from CIDR strings. An out-of-range value (e.g. /200 on an IPv6 range) caused the byte comparison loop to index past inet_pton()'s 16-byte output, producing empty-string equality that could silently return true. An IPv4 /0 prefix also triggered a 32-bit arithmetic shift — undefined behavior on strict 64-bit PHP builds. Both cases now return false immediately via explicit bounds guards (> 32 for IPv4, > 128 for IPv6). A defensive inet_pton() === false short-circuit was also added before the byte loop.
startup() — callback exemption scoped to login task
The plugin.duo_callback action exemption was keyed on the _action parameter alone. A request bearing _action=plugin.duo_callback on any non-login task would suppress the pending-auth check entirely. The exemption is now scoped to $task === 'login', matching the only task under which the callback handler is legitimately dispatched.
startup() — proactive stale session cleanup on login page
Stale duo_state from an abandoned Duo flow was not cleared until the user next accessed a protected task. cleanup_duo_session() is now called inside the login-page early-return branch so the pending state is removed the moment the user returns to the login page.
callback_handler() — single authoritative cleanup owner
cleanup_duo_session() was called redundantly in both catch blocks and inside fail_login(), which is the sole exit path for those branches. The redundant calls are removed; fail_login() is the canonical cleanup point for all error exits.
No Breaking Changes
All configuration options, hook names, session key names, and bypass logic precedence are unchanged. Existing deployments require no configuration updates.
Upgrade
Composer:
composer update lmr/duo_authManual:
Replace duo_auth.php with the v2.0.6 version. No other files changed.
Full Changelog: v2.0.5...v2.0.6