Skip to content

Fix GH-24616: Socket transports accept ports above 65535 - #24621

Open
Marti2203 wants to merge 1 commit into
php:PHP-8.4from
Marti2203:fix-gh24616-socket-port-range
Open

Marti2203 wants to merge 1 commit into
php:PHP-8.4from
Marti2203:fix-gh24616-socket-port-range

Conversation

@Marti2203

Copy link
Copy Markdown
Contributor

parse_ip_address_ex() in main/streams/xp_socket.c reads the port with atoi() and does not check its range. The port is later narrowed to unsigned short, so a port above 65535 (or a negative one) wraps to another port. fsockopen(), stream_socket_client() and stream_socket_server(), for both tcp:// and udp://, connect to or bind the wrong port:

$srv = stream_socket_server("tcp://127.0.0.1:0");
$port = (int) substr(stream_socket_get_name($srv, false), strlen("127.0.0.1:"));
$c = stream_socket_client("tcp://127.0.0.1:" . ($port + 65536));  // connects to $port
$c2 = fsockopen("127.0.0.1", $port + 65536);                      // connects to $port

php_network_parse_network_address_with_port() in main/network.c (used for the target address of stream_socket_sendto()) has the same atoi() pattern, and stores the result in a short.

Fix

Parse the port with ZEND_STRTOL() and reject values outside 0..65535. In parse_ip_address_ex(), this applies to both the plain and the [IPv6]:port forms, and fails with Failed to parse port of address "..." (like the existing Failed to parse address "..." errors). php_network_parse_network_address_with_port() returns FAILURE, so stream_socket_sendto() reports its existing "Failed to parse ... into a valid network address" warning.

Everything else that atoi() accepted is still accepted, for example trailing characters after the port and an empty port (port 0), to keep the change small for a stable branch. An out-of-range port in the bindto context option is now handled like any other bindto value that does not parse (it is ignored, as before for unparsable values).

ext/standard/tests/streams/bug69521.phpt used port 74321 and so relied on this wrap (it actually listened on 8785). It now uses an ephemeral port.

Test

ext/standard/tests/network/gh24616.phpt uses port + 65536 of a listening server with stream_socket_client(), fsockopen() and the IPv6 form. It also tries negative and very large ports, stream_socket_server() on 65536, and stream_socket_sendto() to a UDP port + 65536. Then it checks that the real port still connects.

  • Without the fix: the client and fsockopen() calls return a connected stream, stream_socket_server() binds, and stream_socket_sendto() sends 1 byte.
  • With the fix: all of them fail, and the valid port still works.
  • ext/standard/tests/network, ext/standard/tests/streams and ext/standard/tests/file (debug build): 807 passed, 323 skipped, 0 failed.

This rejects input that was accepted before, so if you prefer to treat it as a behaviour change, I can retarget it to master.

Fixes GH-24616

This issue was found by an LLM-Enhanced Static Analysis Engine and verified manually; the patch was manually reviewed.

🤖 Generated with Claude Code

parse_ip_address_ex() and php_network_parse_network_address_with_port()
read the port with atoi() and did not check its range, so a port above
65535 or below 0 silently wrapped to another port. Reject such ports.

bug69521.phpt relied on this wrapping (port 74321), use an ephemeral
port instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants