Skip to content

Commit

Permalink
Add option to specify the allowed "php" wrapper types
Browse files Browse the repository at this point in the history
In addition of the current possibility to filter wrappers by their
protocol name, also add the option to filter the "php" wrapper by the
requested kind.
Especially the 'filter' backend can be disabled that way.
  • Loading branch information
cgzones committed May 27, 2024
1 parent 7e77b2f commit 235e07c
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 1 deletion.
2 changes: 2 additions & 0 deletions config/default_php8.rules
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ sp.xxe_protection.enable();
# PHP has a lot of wrappers, most of them aren't usually useful, you should
# only enable the ones you're using.
# sp.wrappers_whitelist.list("file,php,phar");
# The "php" wrapper can be further filtered
# sp.wrappers_whitelist.php_list("stdout,stdin,stderr");

# Prevent sloppy comparisons.
# sp.sloppy_comparison.enable();
Expand Down
11 changes: 11 additions & 0 deletions doc/source/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,17 @@ to explicitly whitelist some `stream wrappers <https://secure.php.net/manual/en/
sp.wrappers_whitelist.list("file,php,phar");


Allowlist of the php stream-wrapper
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:ref:`The php-stream-wrapper allowlist <php-stream-wrapper-allowlist-feature>`
allows to explicitly allow the builtin `php streams <https://www.php.net/manual/en/wrappers.php.php>`__.

::

sp.wrappers_whitelist.php_list("stdout,stdin,stderr");


Eval white and blacklist
^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
16 changes: 15 additions & 1 deletion doc/source/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ and using this feature to lock this up.
Whitelist of stream-wrappers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Php comes with a `lot of different <https://secure.php.net/manual/en/wrappers.php>`__
PHP comes with a `lot of different <https://secure.php.net/manual/en/wrappers.php>`__
`stream wrapper <https://secure.php.net/manual/en/intro.stream.php>`__, and most of them
are enabled by default.

Expand All @@ -397,6 +397,20 @@ Examples of related vulnerabilities
- `Data exfiltration via stream wrapper <https://www.idontplaydarts.com/2011/02/using-php-filter-for-local-file-inclusion/>`__
- `Inclusion via zip/phar <https://lightless.me/archives/include-file-from-zip-or-phar.html>`__

.. _php-stream-wrapper-allowlist-feature:

Allowlist of php stream-wrapper
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The builtin `"php" stream wrapper <https://www.php.net/manual/en/wrappers.php.php>`__
has support for common streams, like ``stdin``, ``stdout`` or ``stderr``, but
also for the dangerous ``filter`` one.

Examples of related vulnerability
"""""""""""""""""""""""""""""""""

- `CNEXT exploits <https://github.com/ambionics/cnext-exploits/>`__

.. _eval-feature:

White and blacklist in ``eval``
Expand Down
3 changes: 3 additions & 0 deletions src/snuffleupagus.c
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ static PHP_GINIT_FUNCTION(snuffleupagus) {
SP_INIT_NULL(config_eval.blacklist);
SP_INIT_NULL(config_eval.whitelist);
SP_INIT_NULL(config_wrapper.whitelist);
SP_INIT_NULL(config_wrapper.php_stream_allowlist);
#undef SP_INIT_NULL
}

Expand Down Expand Up @@ -175,6 +176,7 @@ static PHP_GSHUTDOWN_FUNCTION(snuffleupagus) {
FREE_LST(config_eval.blacklist);
FREE_LST(config_eval.whitelist);
FREE_LST(config_wrapper.whitelist);
FREE_LST(config_wrapper.php_stream_allowlist);
#undef FREE_LST


Expand Down Expand Up @@ -388,6 +390,7 @@ static void dump_config() {
add_assoc_bool(&arr, SP_TOKEN_SLOPPY_COMPARISON "." SP_TOKEN_ENABLE, SPCFG(sloppy).enable);

ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS, SPCFG(wrapper).whitelist);
ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_ALLOW_PHP_STREAMS, SPCFG(wrapper).php_stream_allowlist);

#undef ADD_ASSOC_SPLIST

Expand Down
2 changes: 2 additions & 0 deletions src/sp_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ typedef struct {

typedef struct {
sp_list_node *whitelist;
sp_list_node *php_stream_allowlist;
bool enabled;
size_t num_wrapper; // Used to verify if wrappers were added.
} sp_config_wrapper;
Expand Down Expand Up @@ -214,6 +215,7 @@ typedef struct {
#define SP_TOKEN_EVAL_WHITELIST "eval_whitelist"
#define SP_TOKEN_SLOPPY_COMPARISON "sloppy_comparison"
#define SP_TOKEN_ALLOW_WRAPPERS "wrappers_whitelist"
#define SP_TOKEN_ALLOW_PHP_STREAMS "php_list"
#define SP_TOKEN_INI_PROTECTION "ini_protection"
#define SP_TOKEN_INI "ini"

Expand Down
1 change: 1 addition & 0 deletions src/sp_config_keywords.c
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ SP_PARSE_FN(parse_wrapper_whitelist) {

sp_config_keyword config_keywords[] = {
{parse_list, SP_TOKEN_LIST, &cfg->whitelist},
{parse_list, SP_TOKEN_ALLOW_PHP_STREAMS, &cfg->php_stream_allowlist},
{0, 0, 0}};

SP_PROCESS_CONFIG_KEYWORDS_ERR();
Expand Down
135 changes: 135 additions & 0 deletions src/sp_wrapper.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "php_snuffleupagus.h"

#define LOG_FEATURE "wrappers_whitelist"

static bool wrapper_is_whitelisted(const zend_string *const zs) {
const sp_list_node *list = SPCFG(wrapper).whitelist;

Expand All @@ -16,6 +18,131 @@ static bool wrapper_is_whitelisted(const zend_string *const zs) {
return false;
}

static bool sp_php_stream_is_filtered(void) {
const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist;

return list != NULL;
}

static bool sp_php_stream_is_whitelisted(const char *const kind) {
const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist;

while (list) {
if (!strcasecmp(kind, ZSTR_VAL((const zend_string *)list->data))) {
return true;
}
list = list->next;
}
return false;
}

/*
* Adopted from
* https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L176
*/
static php_stream * sp_php_stream_url_wrap_php(php_stream_wrapper *wrapper,
const char *path, const char *mode,
int options, zend_string **opened_path,
php_stream_context *context STREAMS_DC) {
if (!strncasecmp(path, "php://", 6)) {
path += 6;
}

if (!strncasecmp(path, "temp", 4)) {
if (!sp_php_stream_is_whitelisted("temp")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"temp\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "memory")) {
if (!sp_php_stream_is_whitelisted("memory")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"memory\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "output")) {
if (!sp_php_stream_is_whitelisted("output")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"output\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "input")) {
if (!sp_php_stream_is_whitelisted("input")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"input\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "stdin")) {
if (!sp_php_stream_is_whitelisted("stdin")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdin\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "stdout")) {
if (!sp_php_stream_is_whitelisted("stdout")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdout\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "stderr")) {
if (!sp_php_stream_is_whitelisted("stderr")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stderr\" dropped");
return NULL;
}
} else if (!strncasecmp(path, "fd/", 3)) {
if (!sp_php_stream_is_whitelisted("fd")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"fd\" dropped");
return NULL;
}
} else if (!strncasecmp(path, "filter/", 7)) {
if (!sp_php_stream_is_whitelisted("filter")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"filter\" dropped");
return NULL;
}
} else {
sp_log_warn(LOG_FEATURE, "Call to unknown php stream type dropped");
return NULL;
}

extern PHPAPI const php_stream_wrapper php_stream_php_wrapper;

return php_stream_php_wrapper.wops->stream_opener(wrapper, path, mode, options, opened_path, context STREAMS_DC);
}

/*
* Adopted from
* https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L428-L446
*/
static const php_stream_wrapper_ops sp_php_stdio_wops = {
sp_php_stream_url_wrap_php,
NULL, /* close */
NULL, /* fstat */
NULL, /* stat */
NULL, /* opendir */
"PHP",
NULL, /* unlink */
NULL, /* rename */
NULL, /* mkdir */
NULL, /* rmdir */
NULL
};
static const php_stream_wrapper sp_php_stream_php_wrapper = {
&sp_php_stdio_wops,
NULL,
0, /* is_url */
};

static void sp_reregister_php_wrapper(void) {
if (!sp_php_stream_is_filtered()) {
return;
}

if (php_unregister_url_stream_wrapper("php") != SUCCESS) {
sp_log_warn(LOG_FEATURE, "Failed to unregister stream wrapper \"php\"");
return;
}

if (php_register_url_stream_wrapper("php", &sp_php_stream_php_wrapper) != SUCCESS) {
sp_log_warn(LOG_FEATURE, "Failed to register custom stream wrapper \"php\"");
}

sp_log_debug(LOG_FEATURE, "Stream \"php\" successfully re-registered");
}

void sp_disable_wrapper() {
HashTable *orig = php_stream_get_url_stream_wrappers_hash();
HashTable *orig_complete = pemalloc(sizeof(HashTable), 1);
Expand Down Expand Up @@ -50,6 +177,12 @@ PHP_FUNCTION(sp_stream_wrapper_register) {
zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "S*", &protocol_name, &params, &param_count);
// ignore proper arguments here and just let the original handler deal with it
if (!protocol_name || wrapper_is_whitelisted(protocol_name)) {

// reject manual loading of "php" wrapper
if (!strcasecmp(ZSTR_VAL(protocol_name), "php") && sp_php_stream_is_filtered()) {
return;
}

orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("stream_wrapper_register"));
orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
Expand All @@ -61,5 +194,7 @@ int hook_stream_wrappers() {
HOOK_FUNCTION("stream_wrapper_register", sp_internal_functions_hook,
PHP_FN(sp_stream_wrapper_register));

sp_reregister_php_wrapper();

return SUCCESS;
}
2 changes: 2 additions & 0 deletions src/tests/stream_wrapper/config/config_stream_wrapper_php.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sp.wrappers_whitelist.list("php");
sp.wrappers_whitelist.php_list("stdin,stderr,stdout");
76 changes: 76 additions & 0 deletions src/tests/stream_wrapper/stream_wrapper_php.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
--TEST--
Stream wrapper (php)
--SKIPIF--
<?php
if (!extension_loaded("snuffleupagus")) print "skip snuffleupagus extension missing";
?>
--INI--
sp.configuration_file={PWD}/config/config_stream_wrapper_php.ini
--FILE--
<?php
echo file_get_contents('php://input');
file_put_contents('php://output', "Hello from stdout\n");
file_put_contents('php://stderr', "Hello from stderr #1\n");
file_put_contents('php://memory', "Bye from memory\n");
echo file_get_contents('php://memory');
file_put_contents('php://temp', "Bye from temp\n");
echo file_get_contents('php://temp');

file_put_contents('php://stderr', "Hello from stderr #2\n");

file_put_contents('php://filter/write=string.toupper/resource=output.tmp', "Hello from stdout filtered\n");
echo file_get_contents('php://filter/read=string.toupper/resource=output.tmp');

$foo = stream_wrapper_unregister("php");
fwrite(STDERR, $foo);
file_put_contents('php://stderr', "Hello from stderr #3\n");

stream_wrapper_restore("php");
file_put_contents('php://stderr', "Hello from stderr #4\n");
file_put_contents('php://memory', "Bye from memory\n");
?>
--EXPECTF--
Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "input" dropped in %a/stream_wrapper_php.php on line 2

Warning: file_get_contents(php://input): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 2

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "output" dropped in %a/stream_wrapper_php.php on line 3

Warning: file_put_contents(php://output): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 3
Hello from stderr #1

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line 5

Warning: file_put_contents(php://memory): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 5

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line 6

Warning: file_get_contents(php://memory): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 6

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "temp" dropped in %a/stream_wrapper_php.php on line 7

Warning: file_put_contents(php://temp): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 7

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "temp" dropped in %a/stream_wrapper_php.php on line 8

Warning: file_get_contents(php://temp): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 8
Hello from stderr #2

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "filter" dropped in %a/stream_wrapper_php.php on line 12

Warning: file_put_contents(php://filter/write=string.toupper/resource=output.tmp): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 12

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "filter" dropped in %a/stream_wrapper_php.php on line 13

Warning: file_get_contents(php://filter/read=string.toupper/resource=output.tmp): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 13
1
Warning: file_put_contents(): Unable to find the wrapper "php" - did you forget to enable it when you configured PHP? in %a/stream_wrapper_php.php on line 17

Warning: file_put_contents(): file:// wrapper is disabled in the server configuration in %a/stream_wrapper_php.php on line 17

Warning: file_put_contents(php://stderr): Failed to open stream: no suitable wrapper could be found in %a/stream_wrapper_php.php on line 17
Hello from stderr #4

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line 21

Warning: file_put_contents(php://memory): Failed to open stream: operation failed in %a/stream_wrapper_php.php on line 21

0 comments on commit 235e07c

Please sign in to comment.