Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/Dockerfile.build-extension
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ RUN ./configure \
--enable-mbstring \
--enable-pcntl \
--enable-intl \
--enable-sockets \
--with-curl \
--with-mysqli \
--with-openssl \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-extension-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: aikidosec/firewall-php-build-extension
VERSION: v1
VERSION: v1.0.1

jobs:
build-amd64:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
build_php_extension:
name: Build php${{ matrix.php_version }} extension${{ matrix.arch }}
runs-on: ubuntu-24.04${{ matrix.arch }}
container: ghcr.io/aikidosec/firewall-php-build-extension:${{ matrix.php_version }}-v1
container: ghcr.io/aikidosec/firewall-php-build-extension:${{ matrix.php_version }}-v1.0.1
strategy:
matrix:
php_version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ Zen for PHP can do this because the monitored functions are hooked at the PHP-co
* ✅ [`HTTP_Request2`](https://pear.php.net/package/http_request2)
* ✅ [`Symfony\HTTPClient`](https://symfony.com/doc/current/http_client.html)
* ✅ [`file_get_contents`](https://www.php.net/manual/en/function.file-get-contents.php)
* ✅ Socket functions
* ✅ [`socket_connect`](https://www.php.net/manual/en/function.socket-connect.php)
* ✅ [`fsockopen`](https://www.php.net/manual/en/function.fsockopen.php)
* ✅ [`stream_socket_client`](https://www.php.net/manual/en/function.stream-socket-client.php)

## Reporting to your Aikido Security dashboard

Expand Down
154 changes: 154 additions & 0 deletions lib/php-extension/HandleUrls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,157 @@ AIKIDO_HANDLER_FUNCTION(handle_post_curl_exec) {
}

}

AIKIDO_HANDLER_FUNCTION(handle_pre_socket_connect) {
scopedTimer.SetSink(sink, "outgoing_http_op");

zval *socketHandle = NULL;
zval *address = NULL;
zval *port = NULL;
php_socket *phpSock = NULL;

ZEND_PARSE_PARAMETERS_START(0, -1)
Z_PARAM_OPTIONAL
#if PHP_VERSION_ID >= 80000
Z_PARAM_OBJECT(socketHandle)
#else
Z_PARAM_RESOURCE(socketHandle)
#endif
Z_PARAM_ZVAL(address)
#if PHP_VERSION_ID >= 80000
Z_PARAM_ZVAL_OR_NULL(port)
#else
Z_PARAM_ZVAL_EX(port, 0, 1)
#endif
ZEND_PARSE_PARAMETERS_END();

#if PHP_VERSION_ID >= 80000
if (socketHandle) {
phpSock = Z_SOCKET_P(socketHandle);
Comment thread
PopoviciMarian marked this conversation as resolved.
ENSURE_SOCKET_VALID(phpSock);
// if the socket is not an IP address, we return
if (phpSock->type != AF_INET && phpSock->type != AF_INET6) {
return;
}
}
#else
// For PHP 7, we can't access the socket resource type directly
// as php_sockets_le_socket() is not exported. We'll rely on address validation instead.
#endif

std::string addressStr = "";
std::string portStr = "";

if (address && Z_TYPE_P(address) == IS_STRING) {
addressStr = Z_STRVAL_P(address);
} else if (address && Z_TYPE_P(address) == IS_LONG) {
// If address is numeric, it might be an IP address
addressStr = std::to_string(Z_LVAL_P(address));
}

if (port && Z_TYPE_P(port) == IS_LONG && Z_LVAL_P(port) > 0) {
portStr = std::to_string(Z_LVAL_P(port));
} else if (port && Z_TYPE_P(port) == IS_STRING) {
portStr = Z_STRVAL_P(port);
}

if (addressStr.empty()) {
return;
}

if (!portStr.empty()) {
eventCache.outgoingRequestUrl = "tcp://" + addressStr + ":" + portStr;
eventCache.outgoingRequestPort = portStr;
} else {
eventCache.outgoingRequestUrl = "tcp://" + addressStr;
eventCache.outgoingRequestPort = "80";
}

eventId = EVENT_PRE_OUTGOING_REQUEST;
}

AIKIDO_HANDLER_FUNCTION(handle_post_socket_connect) {
Comment thread
PopoviciMarian marked this conversation as resolved.
eventId = EVENT_POST_OUTGOING_REQUEST;
// For socket_connect, we don't have easy access to resolved IP after connection
// The URL was already set in pre handler
eventCache.outgoingRequestEffectiveUrl = eventCache.outgoingRequestUrl;
eventCache.outgoingRequestEffectiveUrlPort = eventCache.outgoingRequestPort;
}

AIKIDO_HANDLER_FUNCTION(handle_pre_fsockopen) {
scopedTimer.SetSink(sink, "outgoing_http_op");

zval *hostname = NULL;
zend_long port = -1;

ZEND_PARSE_PARAMETERS_START(0, -1)
Z_PARAM_OPTIONAL
Z_PARAM_ZVAL(hostname)
Z_PARAM_LONG(port)
ZEND_PARSE_PARAMETERS_END();

std::string hostnameStr = "";
std::string portStr = "";

if (hostname && Z_TYPE_P(hostname) == IS_STRING) {
hostnameStr = Z_STRVAL_P(hostname);
}

if (port >= 0) {
portStr = std::to_string(port);
}

if (!hostnameStr.empty()) {
if (!portStr.empty()) {
eventCache.outgoingRequestUrl = "tcp://" + hostnameStr + ":" + portStr;
eventCache.outgoingRequestPort = portStr;
} else {
eventCache.outgoingRequestUrl = "tcp://" + hostnameStr;
eventCache.outgoingRequestPort = "80"; // Default port
}
}

if (eventCache.outgoingRequestUrl.empty()) return;

eventId = EVENT_PRE_OUTGOING_REQUEST;
}

AIKIDO_HANDLER_FUNCTION(handle_post_fsockopen) {
eventId = EVENT_POST_OUTGOING_REQUEST;
// For fsockopen, we don't have easy access to resolved IP after connection
// The URL was already set in pre handler
eventCache.outgoingRequestEffectiveUrl = eventCache.outgoingRequestUrl;
eventCache.outgoingRequestEffectiveUrlPort = eventCache.outgoingRequestPort;
}

AIKIDO_HANDLER_FUNCTION(handle_pre_stream_socket_client) {
scopedTimer.SetSink(sink, "outgoing_http_op");

zval *address = NULL;

ZEND_PARSE_PARAMETERS_START(0, -1)
Z_PARAM_OPTIONAL
Z_PARAM_ZVAL(address)
ZEND_PARSE_PARAMETERS_END();

std::string addressStr = "";

if (address && Z_TYPE_P(address) == IS_STRING) {
addressStr = Z_STRVAL_P(address);
}

if (addressStr.empty()){
return;
}

eventCache.outgoingRequestUrl = addressStr;
eventId = EVENT_PRE_OUTGOING_REQUEST;
}

AIKIDO_HANDLER_FUNCTION(handle_post_stream_socket_client) {
eventId = EVENT_POST_OUTGOING_REQUEST;
// For stream_socket_client, we don't have easy access to resolved IP after connection
// The URL was already set in pre handler
eventCache.outgoingRequestEffectiveUrl = eventCache.outgoingRequestUrl;
eventCache.outgoingRequestEffectiveUrlPort = eventCache.outgoingRequestPort;
}
3 changes: 3 additions & 0 deletions lib/php-extension/Hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
unordered_map<std::string, PHP_HANDLERS> HOOKED_FUNCTIONS = {
/* Outgoing request */
AIKIDO_REGISTER_FUNCTION_HANDLER_WITH_POST(curl_exec),
AIKIDO_REGISTER_FUNCTION_HANDLER_WITH_POST_EX(socket_connect, handle_pre_socket_connect, handle_post_socket_connect),
AIKIDO_REGISTER_FUNCTION_HANDLER_WITH_POST_EX(fsockopen, handle_pre_fsockopen, handle_post_fsockopen),
AIKIDO_REGISTER_FUNCTION_HANDLER_WITH_POST_EX(stream_socket_client, handle_pre_stream_socket_client, handle_post_stream_socket_client),

/* Shell execution */
AIKIDO_REGISTER_FUNCTION_HANDLER_EX(exec, handle_shell_execution),
Expand Down
7 changes: 7 additions & 0 deletions lib/php-extension/include/HandleUrls.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@

AIKIDO_HANDLER_FUNCTION(handle_pre_curl_exec);
AIKIDO_HANDLER_FUNCTION(handle_post_curl_exec);

AIKIDO_HANDLER_FUNCTION(handle_pre_socket_connect);
AIKIDO_HANDLER_FUNCTION(handle_post_socket_connect);
AIKIDO_HANDLER_FUNCTION(handle_pre_fsockopen);
AIKIDO_HANDLER_FUNCTION(handle_post_fsockopen);
AIKIDO_HANDLER_FUNCTION(handle_pre_stream_socket_client);
AIKIDO_HANDLER_FUNCTION(handle_post_stream_socket_client);
1 change: 1 addition & 0 deletions lib/php-extension/include/Includes.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ using json = nlohmann::json;
#include "php.h"
#include "zend_exceptions.h"
#include "zend_builtin_functions.h"
#include "ext/sockets/php_sockets.h"

#include "GoCGO.h"
#include "GoWrappers.h"
Expand Down
80 changes: 80 additions & 0 deletions tests/cli/outgoing_request/test_socket.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
--TEST--
Test socket functions (socket_connect, fsockopen, stream_socket_client)

--SKIPIF--
<?php
if (PHP_VERSION_ID >= 80500) {
die("skip PHP >= 8.5.");
}
if (!extension_loaded('sockets')) {
die("skip sockets extension not loaded");
}
?>

--ENV--
AIKIDO_LOG_LEVEL=INFO

--FILE--
<?php
// Test socket_connect
$socket1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket1 !== false) {
@socket_connect($socket1, "example.com", 443);
socket_close($socket1);
}

// Test fsockopen
$fp1 = @fsockopen("httpbin.org", 443, $errno, $errstr, 1);
if ($fp1) {
fclose($fp1);
}

// Test stream_socket_client
$fp2 = @stream_socket_client("tcp://facebook.com:443", $errno, $errstr, 1);
if ($fp2) {
fclose($fp2);
}

// Test socket_connect with explicit port
$socket2 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket2 !== false) {
@socket_connect($socket2, "www.aikido.dev", 80);
socket_close($socket2);
}

// Test fsockopen with explicit port
$fp3 = @fsockopen("some-invalid-domain.com", 4113, $errno, $errstr, 1);
if ($fp3) {
fclose($fp3);
}

// Test stream_socket_client with http:// scheme
$fp4 = @stream_socket_client("http://www.aikido.dev:80", $errno, $errstr, 1);
if ($fp4) {
fclose($fp4);
}

// Test stream_socket_client with https:// scheme
$fp5 = @stream_socket_client("https://example.com:443", $errno, $errstr, 1);
if ($fp5) {
fclose($fp5);
}

?>

--EXPECT--
[AIKIDO][INFO] [BEFORE] Got domain: example.com
[AIKIDO][INFO] [AFTER] Got domain: example.com port: 443
[AIKIDO][INFO] [BEFORE] Got domain: httpbin.org
[AIKIDO][INFO] [AFTER] Got domain: httpbin.org port: 443
[AIKIDO][INFO] [BEFORE] Got domain: facebook.com
[AIKIDO][INFO] [AFTER] Got domain: facebook.com port: 443
[AIKIDO][INFO] [BEFORE] Got domain: www.aikido.dev
[AIKIDO][INFO] [AFTER] Got domain: www.aikido.dev port: 80
[AIKIDO][INFO] [BEFORE] Got domain: some-invalid-domain.com
[AIKIDO][INFO] [AFTER] Got domain: some-invalid-domain.com port: 4113
[AIKIDO][INFO] [BEFORE] Got domain: www.aikido.dev
[AIKIDO][INFO] [AFTER] Got domain: www.aikido.dev port: 80
[AIKIDO][INFO] [BEFORE] Got domain: example.com
[AIKIDO][INFO] [AFTER] Got domain: example.com port: 443

11 changes: 11 additions & 0 deletions tests/server/test_ssrf_socket/change_config_disable_blocking.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"success": true,
"serviceId": 1,
"heartbeatIntervalInMS": 600000,
"endpoints": [],
"blockedUserIds": [],
"allowedIPAddresses": [],
"receivedAnyStats": true,
"block": false
}

6 changes: 6 additions & 0 deletions tests/server/test_ssrf_socket/env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"AIKIDO_BLOCK": "1",
"AIKIDO_LOCALHOST_ALLOWED_BY_DEFAULT": "0",
"AIKIDO_FEATURE_COLLECT_API_SCHEMA": "1"
}

36 changes: 36 additions & 0 deletions tests/server/test_ssrf_socket/expect_detection_blocked.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"type": "detected_attack",
"request": {
"headers": {
"content_type": [
"application/json"
]
},
"method": "POST",
"body": "{\"url\": \"http://127.0.0.1:8081\", \"method\": \"socket_connect\"}",
"source": "php",
"route": "/testDetection"
},
"attack": {
"kind": "ssrf",
"operation": "socket_connect",
"blocked": true,
"source": "body",
"path": ".url",
"stack": "tests/server/test_ssrf_socket/index.php(20): socket_connect()",
"payload": "http://127.0.0.1:8081",
"metadata": {
"hostname": "127.0.0.1",
"port": "8081"
},
"user": {
"id": "12345",
"name": "Tudor"
}
},
"agent": {
"dryMode": false,
"library": "firewall-php"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"type": "detected_attack",
"request": {
"headers": {
"content_type": [
"application/json"
]
},
"method": "POST",
"body": "{\"url\": \"http://127.0.0.1:8081\", \"method\": \"fsockopen\"}",
"source": "php",
"route": "/testDetection"
},
"attack": {
"kind": "ssrf",
"operation": "fsockopen",
"blocked": true,
"source": "body",
"path": ".url",
"stack": "tests/server/test_ssrf_socket/index.php(27): fsockopen()",
"payload": "http://127.0.0.1:8081",
"metadata": {
"hostname": "127.0.0.1",
"port": "8081"
},
"user": {
"id": "12345",
"name": "Tudor"
}
},
"agent": {
"dryMode": false,
"library": "firewall-php"
}
}

Loading
Loading