summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/config.rst14
-rw-r--r--doc/source/features.rst2
-rw-r--r--src/php_snuffleupagus.h1
-rw-r--r--src/snuffleupagus.c8
-rw-r--r--src/sp_config.c1
-rw-r--r--src/sp_config.h6
-rw-r--r--src/sp_config_keywords.c18
-rw-r--r--src/sp_config_keywords.h1
-rw-r--r--src/sp_unserialize.c30
-rw-r--r--src/tests/unserialize/config/config_serialize_noclass.ini1
-rw-r--r--src/tests/unserialize/unserialize_noclass_forced.phpt21
-rw-r--r--src/tests/unserialize/unserialize_wrong_call.phpt2
-rw-r--r--src/tests/unserialize_php8/config/config_serialize_noclass.ini1
-rw-r--r--src/tests/unserialize_php8/config/config_serialize_noclass_disabled.ini1
-rw-r--r--src/tests/unserialize_php8/unserialize_noclass_forced.phpt38
-rw-r--r--src/tests/unserialize_php8/unserialize_noclass_forced_disabled.phpt35
16 files changed, 167 insertions, 13 deletions
diff --git a/doc/source/config.rst b/doc/source/config.rst
index 9d2d0ed..bce4667 100644
--- a/doc/source/config.rst
+++ b/doc/source/config.rst
@@ -202,6 +202,20 @@ It can either be ``enabled`` or ``disabled``.
202 sp.sloppy_comparison.enable(); 202 sp.sloppy_comparison.enable();
203 sp.sloppy_comparison.disable(); 203 sp.sloppy_comparison.disable();
204 204
205unserialize_noclass
206^^^^^^^^^^^^^^^^^^^
207
208:ref:`unserialize_noclass <unserialize-feature>`, available only on PHP8+ and
209disabled by default, will disable the deserialization of objects via
210``unserialize``. It's equivalent to setting the ``options`` parameter of
211``unserialize`` to ``false``, on every call. It can either be ``enabled`` or
212``disabled``.
213
214::
215
216 sp.unserialize_noclass.enable();
217 sp.unserialize_noclass.disable();
218
205unserialize_hmac 219unserialize_hmac
206^^^^^^^^^^^^^^^^ 220^^^^^^^^^^^^^^^^
207 221
diff --git a/doc/source/features.rst b/doc/source/features.rst
index 25fd62d..60dbbef 100644
--- a/doc/source/features.rst
+++ b/doc/source/features.rst
@@ -166,6 +166,8 @@ CVE-2016-9138 <https://bugs.php.net/bug.php?id=73147>`_, `2016-7124
166<https://bugs.php.net/bug.php?id=72663>`_, `CVE-2016-5771 and CVE-2016-5773 166<https://bugs.php.net/bug.php?id=72663>`_, `CVE-2016-5771 and CVE-2016-5773
167<https://www.evonide.com/how-we-broke-php-hacked-pornhub-and-earned-20000-dollar/>`_. 167<https://www.evonide.com/how-we-broke-php-hacked-pornhub-and-earned-20000-dollar/>`_.
168 168
169A less subtle mitigation can be used to simply prevent the deserialization of objects altogether.
170
169 171
170Examples of related vulnerabilities 172Examples of related vulnerabilities
171""""""""""""""""""""""""""""""""""" 173"""""""""""""""""""""""""""""""""""
diff --git a/src/php_snuffleupagus.h b/src/php_snuffleupagus.h
index 7c534a8..2117462 100644
--- a/src/php_snuffleupagus.h
+++ b/src/php_snuffleupagus.h
@@ -119,6 +119,7 @@ ZEND_BEGIN_MODULE_GLOBALS(snuffleupagus)
119sp_config_random config_random; 119sp_config_random config_random;
120sp_config_sloppy config_sloppy; 120sp_config_sloppy config_sloppy;
121sp_config_unserialize config_unserialize; 121sp_config_unserialize config_unserialize;
122sp_config_unserialize_noclass config_unserialize_noclass;
122sp_config_readonly_exec config_readonly_exec; 123sp_config_readonly_exec config_readonly_exec;
123sp_config_upload_validation config_upload_validation; 124sp_config_upload_validation config_upload_validation;
124sp_config_cookie config_cookie; 125sp_config_cookie config_cookie;
diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c
index be6240e..8454fc1 100644
--- a/src/snuffleupagus.c
+++ b/src/snuffleupagus.c
@@ -332,6 +332,8 @@ static void dump_config() {
332 array_init(&arr); 332 array_init(&arr);
333 add_assoc_string(&arr, "version", PHP_SNUFFLEUPAGUS_VERSION); 333 add_assoc_string(&arr, "version", PHP_SNUFFLEUPAGUS_VERSION);
334 334
335 add_assoc_bool(&arr, SP_TOKEN_UNSERIALIZE_NOCLASS "." SP_TOKEN_ENABLE, SPCFG(unserialize_noclass).enable);
336
335 add_assoc_bool(&arr, SP_TOKEN_UNSERIALIZE_HMAC "." SP_TOKEN_ENABLE, SPCFG(unserialize).enable); 337 add_assoc_bool(&arr, SP_TOKEN_UNSERIALIZE_HMAC "." SP_TOKEN_ENABLE, SPCFG(unserialize).enable);
336 add_assoc_bool(&arr, SP_TOKEN_UNSERIALIZE_HMAC "." SP_TOKEN_SIM, SPCFG(unserialize).simulation); 338 add_assoc_bool(&arr, SP_TOKEN_UNSERIALIZE_HMAC "." SP_TOKEN_SIM, SPCFG(unserialize).simulation);
337 ADD_ASSOC_ZSTR(&arr, SP_TOKEN_UNSERIALIZE_HMAC "." SP_TOKEN_DUMP, SPCFG(unserialize).dump); 339 ADD_ASSOC_ZSTR(&arr, SP_TOKEN_UNSERIALIZE_HMAC "." SP_TOKEN_DUMP, SPCFG(unserialize).dump);
@@ -562,10 +564,8 @@ static PHP_INI_MH(OnUpdateConfiguration) {
562 hook_session(); 564 hook_session();
563 } 565 }
564 566
565 if (NULL != SPCFG(encryption_key)) { 567 if ((NULL != SPCFG(encryption_key) && SPCFG(unserialize).enable) || SPCFG(unserialize_noclass).enable) {
566 if (SPCFG(unserialize).enable) { 568 hook_serialize();
567 hook_serialize();
568 }
569 } 569 }
570 570
571 hook_execute(); 571 hook_execute();
diff --git a/src/sp_config.c b/src/sp_config.c
index dbccb1a..8bd238a 100644
--- a/src/sp_config.c
+++ b/src/sp_config.c
@@ -8,6 +8,7 @@
8static zend_result sp_process_config_root(sp_parsed_keyword *parsed_rule) { 8static zend_result sp_process_config_root(sp_parsed_keyword *parsed_rule) {
9 sp_config_keyword sp_func[] = { 9 sp_config_keyword sp_func[] = {
10 {parse_unserialize, SP_TOKEN_UNSERIALIZE_HMAC, &(SPCFG(unserialize))}, 10 {parse_unserialize, SP_TOKEN_UNSERIALIZE_HMAC, &(SPCFG(unserialize))},
11 {parse_unserialize_noclass, SP_TOKEN_UNSERIALIZE_NOCLASS, &(SPCFG(unserialize_noclass))},
11 {parse_enable, SP_TOKEN_HARDEN_RANDOM, &(SPCFG(random).enable)}, 12 {parse_enable, SP_TOKEN_HARDEN_RANDOM, &(SPCFG(random).enable)},
12 {parse_log_media, SP_TOKEN_LOG_MEDIA, &(SPCFG(log_media))}, 13 {parse_log_media, SP_TOKEN_LOG_MEDIA, &(SPCFG(log_media))},
13 {parse_disabled_functions, SP_TOKEN_DISABLE_FUNC, NULL}, 14 {parse_disabled_functions, SP_TOKEN_DISABLE_FUNC, NULL},
diff --git a/src/sp_config.h b/src/sp_config.h
index ec73310..cddf816 100644
--- a/src/sp_config.h
+++ b/src/sp_config.h
@@ -83,6 +83,11 @@ typedef struct {
83 83
84typedef struct { 84typedef struct {
85 bool enable; 85 bool enable;
86 zend_string *textual_representation;
87} sp_config_unserialize_noclass;
88
89typedef struct {
90 bool enable;
86 bool simulation; 91 bool simulation;
87 zend_string *dump; 92 zend_string *dump;
88 zend_string *textual_representation; 93 zend_string *textual_representation;
@@ -202,6 +207,7 @@ typedef struct {
202#define SP_TOKEN_HARDEN_RANDOM "harden_random" 207#define SP_TOKEN_HARDEN_RANDOM "harden_random"
203#define SP_TOKEN_READONLY_EXEC "readonly_exec" 208#define SP_TOKEN_READONLY_EXEC "readonly_exec"
204#define SP_TOKEN_UNSERIALIZE_HMAC "unserialize_hmac" 209#define SP_TOKEN_UNSERIALIZE_HMAC "unserialize_hmac"
210#define SP_TOKEN_UNSERIALIZE_NOCLASS "unserialize_noclass"
205#define SP_TOKEN_UPLOAD_VALIDATION "upload_validation" 211#define SP_TOKEN_UPLOAD_VALIDATION "upload_validation"
206#define SP_TOKEN_XXE_PROTECTION "xxe_protection" 212#define SP_TOKEN_XXE_PROTECTION "xxe_protection"
207#define SP_TOKEN_EVAL_BLACKLIST "eval_blacklist" 213#define SP_TOKEN_EVAL_BLACKLIST "eval_blacklist"
diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c
index fa26635..ff834dd 100644
--- a/src/sp_config_keywords.c
+++ b/src/sp_config_keywords.c
@@ -74,6 +74,24 @@ SP_PARSEKW_FN(parse_log_media) {
74 return SP_PARSER_ERROR; 74 return SP_PARSER_ERROR;
75} 75}
76 76
77SP_PARSE_FN(parse_unserialize_noclass) {
78 bool enable = false, disable = false;
79 sp_config_unserialize_noclass *cfg = (sp_config_unserialize_noclass*)retval;
80
81 sp_config_keyword config_keywords[] = {
82 {parse_empty, SP_TOKEN_ENABLE, &(enable)},
83 {parse_empty, SP_TOKEN_DISABLE, &(disable)},
84 {0, 0, 0}};
85
86 SP_PROCESS_CONFIG_KEYWORDS_ERR();
87
88 SP_SET_ENABLE_DISABLE(enable, disable, cfg->enable);
89
90 cfg->textual_representation = sp_get_textual_representation(parsed_rule);
91
92 return SP_PARSER_STOP;
93}
94
77SP_PARSE_FN(parse_unserialize) { 95SP_PARSE_FN(parse_unserialize) {
78 bool enable = false, disable = false; 96 bool enable = false, disable = false;
79 sp_config_unserialize *cfg = (sp_config_unserialize*)retval; 97 sp_config_unserialize *cfg = (sp_config_unserialize*)retval;
diff --git a/src/sp_config_keywords.h b/src/sp_config_keywords.h
index 01eb0d1..27050e3 100644
--- a/src/sp_config_keywords.h
+++ b/src/sp_config_keywords.h
@@ -6,6 +6,7 @@ SP_PARSE_FN(parse_enable);
6SP_PARSE_FN(parse_global); 6SP_PARSE_FN(parse_global);
7SP_PARSE_FN(parse_cookie); 7SP_PARSE_FN(parse_cookie);
8SP_PARSE_FN(parse_unserialize); 8SP_PARSE_FN(parse_unserialize);
9SP_PARSE_FN(parse_unserialize_noclass);
9SP_PARSE_FN(parse_readonly_exec); 10SP_PARSE_FN(parse_readonly_exec);
10SP_PARSE_FN(parse_disabled_functions); 11SP_PARSE_FN(parse_disabled_functions);
11SP_PARSE_FN(parse_upload_validation); 12SP_PARSE_FN(parse_upload_validation);
diff --git a/src/sp_unserialize.c b/src/sp_unserialize.c
index e57ef9c..641d989 100644
--- a/src/sp_unserialize.c
+++ b/src/sp_unserialize.c
@@ -61,6 +61,10 @@ PHP_FUNCTION(sp_serialize) {
61 orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); 61 orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
62 } 62 }
63 63
64 if (!SPCFG(unserialize).enable) {
65 return;
66 }
67
64 /* Compute the HMAC of the textual representation of the serialized data*/ 68 /* Compute the HMAC of the textual representation of the serialized data*/
65 zend_string *hmac = sp_do_hash_hmac_sha256(Z_STRVAL_P(return_value), Z_STRLEN_P(return_value), ZSTR_VAL(SPCFG(encryption_key)), ZSTR_LEN(SPCFG(encryption_key))); 69 zend_string *hmac = sp_do_hash_hmac_sha256(Z_STRVAL_P(return_value), Z_STRLEN_P(return_value), ZSTR_VAL(SPCFG(encryption_key)), ZSTR_LEN(SPCFG(encryption_key)));
66 70
@@ -84,29 +88,37 @@ PHP_FUNCTION(sp_serialize) {
84} 88}
85 89
86PHP_FUNCTION(sp_unserialize) { 90PHP_FUNCTION(sp_unserialize) {
87 zif_handler orig_handler;
88
89 char *buf = NULL; 91 char *buf = NULL;
90 char *serialized_str = NULL;
91 char *hmac = NULL;
92 size_t buf_len = 0; 92 size_t buf_len = 0;
93 HashTable *opts = NULL; 93 HashTable *opts = NULL;
94 94
95 const sp_config_unserialize *config_unserialize = &(SPCFG(unserialize));
96
97 ZEND_PARSE_PARAMETERS_START(1, 2) 95 ZEND_PARSE_PARAMETERS_START(1, 2)
98 Z_PARAM_STRING(buf, buf_len) 96 Z_PARAM_STRING(buf, buf_len)
99 Z_PARAM_OPTIONAL 97 Z_PARAM_OPTIONAL
100 Z_PARAM_ARRAY_HT(opts) 98 Z_PARAM_ARRAY_HT(opts)
101 ZEND_PARSE_PARAMETERS_END(); 99 ZEND_PARSE_PARAMETERS_END();
102 100
101 if (SPCFG(unserialize_noclass).enable) {
102#if PHP_VERSION_ID > 80000
103 HashTable ht;
104 zend_hash_init(&ht, 1, NULL, NULL, 0);
105 zval zv;
106 ZVAL_FALSE(&zv);
107 zend_hash_str_add(&ht, "allowed_classes", sizeof("allowed_classes")-1, &zv);
108 php_unserialize_with_options(return_value, buf, buf_len, &ht, "unserialize");
109 return;
110#else
111 sp_log_drop("unserialize_noclass", "unserialize_noclass is only supported on PHP8+");
112#endif
113 }
114
103 /* 64 is the length of HMAC-256 */ 115 /* 64 is the length of HMAC-256 */
104 if (buf_len < 64) { 116 if (buf_len < 64) {
105 sp_log_drop("unserialize", "The serialized object is too small."); 117 sp_log_drop("unserialize", "The serialized object is too small.");
106 } 118 }
107 119
108 hmac = buf + buf_len - 64; 120 char* hmac = buf + buf_len - 64;
109 serialized_str = ecalloc(buf_len - 64 + 1, 1); 121 char* serialized_str = ecalloc(buf_len - 64 + 1, 1);
110 memcpy(serialized_str, buf, buf_len - 64); 122 memcpy(serialized_str, buf, buf_len - 64);
111 123
112 zend_string *expected_hmac = sp_do_hash_hmac_sha256(serialized_str, strlen(serialized_str), ZSTR_VAL(SPCFG(encryption_key)), ZSTR_LEN(SPCFG(encryption_key))); 124 zend_string *expected_hmac = sp_do_hash_hmac_sha256(serialized_str, strlen(serialized_str), ZSTR_VAL(SPCFG(encryption_key)), ZSTR_LEN(SPCFG(encryption_key)));
@@ -118,11 +130,13 @@ PHP_FUNCTION(sp_unserialize) {
118 } 130 }
119 } else { status = 1; } 131 } else { status = 1; }
120 132
133 zif_handler orig_handler;
121 if (0 == status) { 134 if (0 == status) {
122 if ((orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("unserialize")))) { 135 if ((orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("unserialize")))) {
123 orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); 136 orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
124 } 137 }
125 } else { 138 } else {
139 const sp_config_unserialize *config_unserialize = &(SPCFG(unserialize));
126 if (config_unserialize->dump) { 140 if (config_unserialize->dump) {
127 sp_log_request(config_unserialize->dump, 141 sp_log_request(config_unserialize->dump,
128 config_unserialize->textual_representation); 142 config_unserialize->textual_representation);
diff --git a/src/tests/unserialize/config/config_serialize_noclass.ini b/src/tests/unserialize/config/config_serialize_noclass.ini
new file mode 100644
index 0000000..b84de51
--- /dev/null
+++ b/src/tests/unserialize/config/config_serialize_noclass.ini
@@ -0,0 +1 @@
sp.unserialize_noclass.enable();
diff --git a/src/tests/unserialize/unserialize_noclass_forced.phpt b/src/tests/unserialize/unserialize_noclass_forced.phpt
new file mode 100644
index 0000000..3b1e8d3
--- /dev/null
+++ b/src/tests/unserialize/unserialize_noclass_forced.phpt
@@ -0,0 +1,21 @@
1--TEST--
2Unserialize with noclass forced
3--SKIPIF--
4<?php if (!extension_loaded("snuffleupagus") || PHP_VERSION_ID >= 80000) print "skip"; ?>
5--INI--
6sp.configuration_file={PWD}/config/config_serialize_noclass.ini
7--FILE--
8<?php
9class C {
10 public $name;
11}
12$c = new C;
13$c->name = "test";
14
15$a = serialize($c);
16var_dump(unserialize($a, ['allowed_classes' => false]));
17var_dump(unserialize($a, ['allowed_classes' => true ]));
18var_dump(unserialize($a));
19?>
20--EXPECTF--
21Fatal error: [snuffleupagus][0.0.0.0][unserialize_noclass][drop] unserialize_noclass is only supported on PHP8+ in %s/tests/unserialize/unserialize_noclass_forced.php on line 9
diff --git a/src/tests/unserialize/unserialize_wrong_call.phpt b/src/tests/unserialize/unserialize_wrong_call.phpt
index a6fe140..afa42f6 100644
--- a/src/tests/unserialize/unserialize_wrong_call.phpt
+++ b/src/tests/unserialize/unserialize_wrong_call.phpt
@@ -12,4 +12,4 @@ var_dump(unserialize($a, "too", "many", "aaaaaaaarguments!"));
12?> 12?>
13--EXPECTF-- 13--EXPECTF--
14Warning: unserialize() expects at most 2 parameters, 4 given in %a/unserialize_wrong_call.php on line %d 14Warning: unserialize() expects at most 2 parameters, 4 given in %a/unserialize_wrong_call.php on line %d
15bool(false) 15NULL
diff --git a/src/tests/unserialize_php8/config/config_serialize_noclass.ini b/src/tests/unserialize_php8/config/config_serialize_noclass.ini
new file mode 100644
index 0000000..b84de51
--- /dev/null
+++ b/src/tests/unserialize_php8/config/config_serialize_noclass.ini
@@ -0,0 +1 @@
sp.unserialize_noclass.enable();
diff --git a/src/tests/unserialize_php8/config/config_serialize_noclass_disabled.ini b/src/tests/unserialize_php8/config/config_serialize_noclass_disabled.ini
new file mode 100644
index 0000000..0238772
--- /dev/null
+++ b/src/tests/unserialize_php8/config/config_serialize_noclass_disabled.ini
@@ -0,0 +1 @@
sp.unserialize_noclass.disable();
diff --git a/src/tests/unserialize_php8/unserialize_noclass_forced.phpt b/src/tests/unserialize_php8/unserialize_noclass_forced.phpt
new file mode 100644
index 0000000..9f276c5
--- /dev/null
+++ b/src/tests/unserialize_php8/unserialize_noclass_forced.phpt
@@ -0,0 +1,38 @@
1--TEST--
2Unserialize with noclass forced
3--SKIPIF--
4<?php if (!extension_loaded("snuffleupagus")) print "skip"; ?>
5--INI--
6sp.configuration_file={PWD}/config/config_serialize_noclass.ini
7--FILE--
8<?php
9class C {
10 public $name;
11}
12$c = new C;
13$c->name = "test";
14
15$a = serialize($c);
16var_dump(unserialize($a, ['allowed_classes' => false]));
17var_dump(unserialize($a, ['allowed_classes' => true ]));
18var_dump(unserialize($a));
19?>
20--EXPECT--
21object(__PHP_Incomplete_Class)#2 (2) {
22 ["__PHP_Incomplete_Class_Name"]=>
23 string(1) "C"
24 ["name"]=>
25 string(4) "test"
26}
27object(__PHP_Incomplete_Class)#2 (2) {
28 ["__PHP_Incomplete_Class_Name"]=>
29 string(1) "C"
30 ["name"]=>
31 string(4) "test"
32}
33object(__PHP_Incomplete_Class)#2 (2) {
34 ["__PHP_Incomplete_Class_Name"]=>
35 string(1) "C"
36 ["name"]=>
37 string(4) "test"
38}
diff --git a/src/tests/unserialize_php8/unserialize_noclass_forced_disabled.phpt b/src/tests/unserialize_php8/unserialize_noclass_forced_disabled.phpt
new file mode 100644
index 0000000..2c4223a
--- /dev/null
+++ b/src/tests/unserialize_php8/unserialize_noclass_forced_disabled.phpt
@@ -0,0 +1,35 @@
1--TEST--
2Unserialize with noclass forced disabled
3--SKIPIF--
4<?php if (!extension_loaded("snuffleupagus")) print "skip"; ?>
5--INI--
6sp.configuration_file={PWD}/config/config_serialize_noclass_disabled.ini
7--FILE--
8<?php
9class C {
10 public $name;
11}
12$c = new C;
13$c->name = "test";
14
15$a = serialize($c);
16var_dump(unserialize($a, ['allowed_classes' => false]));
17var_dump(unserialize($a, ['allowed_classes' => true ]));
18var_dump(unserialize($a));
19?>
20--EXPECT--
21object(__PHP_Incomplete_Class)#2 (2) {
22 ["__PHP_Incomplete_Class_Name"]=>
23 string(1) "C"
24 ["name"]=>
25 string(4) "test"
26}
27object(C)#2 (1) {
28 ["name"]=>
29 string(4) "test"
30}
31object(C)#2 (1) {
32 ["name"]=>
33 string(4) "test"
34}
35