fix: block IPv4-mapped IPv6 SSRF bypass (F-1 HIGH) #2

Merged
eric merged 1 commit from fix/ipv4-mapped-ipv6-ssrf into main 2026-06-27 22:18:02 -07:00
Owner

Summary

  • PHP's `FILTER_FLAG_NO_PRIV_RANGE` and `FILTER_FLAG_NO_RES_RANGE` do not cover `::ffff:0:0/96`, so IPv4-mapped addresses like `::ffff:a9fe:a9fe` (169.254.169.254) and `::ffff:7f00:1` (127.0.0.1) bypassed both the loopback check and private/reserved range check in the SSRF protection stack
  • Fix unwraps IPv4-mapped IPv6 addresses via `inet_pton` packed-byte prefix check before range validation in `resolve_and_validate_url()`, and recurses through `is_loopback_address()` for the loopback path
  • Mirrors the same unwrapping logic used by WordPress core's `wp_http_validate_url()`

Attack scenario (blocked by this fix)

Admin sets API URL to `http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/`. Without this fix, the plugin POSTs to that address, which the OS kernel routes to 169.254.169.254 (AWS EC2 instance metadata), returning IAM credentials.

Payload IPv4 equivalent Target
`http://[::ffff:7f00:1]:6379/` 127.0.0.1:6379 Local Redis
`http://[::ffff:a9fe:a9fe]/latest/meta-data/` 169.254.169.254 AWS IMDS
`http://[::ffff:c0a8:0001]/` 192.168.0.1 RFC 1918 LAN

Changes

`includes/class-apprise-client.php` — two locations patched:

  1. `resolve_and_validate_url()`: unwrap before `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE` check
  2. `is_loopback_address()`: unwrap and recurse before `::1` comparison

Test plan

## Summary - PHP's \`FILTER_FLAG_NO_PRIV_RANGE\` and \`FILTER_FLAG_NO_RES_RANGE\` do not cover \`::ffff:0:0/96\`, so IPv4-mapped addresses like \`::ffff:a9fe:a9fe\` (169.254.169.254) and \`::ffff:7f00:1\` (127.0.0.1) bypassed both the loopback check and private/reserved range check in the SSRF protection stack - Fix unwraps IPv4-mapped IPv6 addresses via \`inet_pton\` packed-byte prefix check before range validation in \`resolve_and_validate_url()\`, and recurses through \`is_loopback_address()\` for the loopback path - Mirrors the same unwrapping logic used by WordPress core's \`wp_http_validate_url()\` ## Attack scenario (blocked by this fix) Admin sets API URL to \`http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/\`. Without this fix, the plugin POSTs to that address, which the OS kernel routes to 169.254.169.254 (AWS EC2 instance metadata), returning IAM credentials. | Payload | IPv4 equivalent | Target | |---------|----------------|--------| | \`http://[::ffff:7f00:1]:6379/\` | 127.0.0.1:6379 | Local Redis | | \`http://[::ffff:a9fe:a9fe]/latest/meta-data/\` | 169.254.169.254 | AWS IMDS | | \`http://[::ffff:c0a8:0001]/\` | 192.168.0.1 | RFC 1918 LAN | ## Changes \`includes/class-apprise-client.php\` — two locations patched: 1. \`resolve_and_validate_url()\`: unwrap before \`FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE\` check 2. \`is_loopback_address()\`: unwrap and recurse before \`::1\` comparison ## Test plan - [ ] Set API URL to \`http://[::ffff:7f00:1]/notify/\` — save should succeed, notification dispatch should return \`localhost_blocked\` WP_Error - [ ] Set API URL to \`http://[::ffff:a9fe:a9fe]/latest/meta-data/\` — returns \`private_ip_blocked\` WP_Error - [ ] Set API URL to a legitimate public IPv6 address — still dispatches normally - [ ] Set API URL to \`::1\` (pure loopback) — still blocked - [ ] "Allow Private IPs" enabled + \`http://[::ffff:c0a8:0001]/\` — passes through (private allowed)
PHP's FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE do not
cover the ::ffff:0:0/96 range, so addresses like ::ffff:169.254.169.254
(AWS IMDS) and ::ffff:7f00:1 (127.0.0.1) bypassed both the loopback
check and the private/reserved range check entirely.

Fix: unwrap IPv4-mapped IPv6 addresses via inet_pton packed-byte prefix
check before range validation in resolve_and_validate_url(), and recurse
through is_loopback_address() for the loopback check. Mirrors the
unwrapping logic in WordPress core's wp_http_validate_url().

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
eric merged commit aeb612efab into main 2026-06-27 22:18:02 -07:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eric/apprise-wordpress-notifications!2
No description provided.