1 #include "php_snuffleupagus.h"
2 
sp_lookup_cookie_config(const zend_string * key)3 static inline const sp_cookie *sp_lookup_cookie_config(const zend_string *key) {
4   const sp_list_node *it = SNUFFLEUPAGUS_G(config).config_cookie->cookies;
5 
6   while (it) {
7     const sp_cookie *config = it->data;
8     if (config && sp_match_value(key, config->name, config->name_r)) {
9       return config;
10     }
11     it = it->next;
12   }
13   return NULL;
14 }
15 
16 /* called at RINIT time with each cookie, eventually decrypt said cookie */
decrypt_cookie(zval * pDest,int num_args,va_list args,zend_hash_key * hash_key)17 int decrypt_cookie(zval *pDest, int num_args, va_list args,
18                    zend_hash_key *hash_key) {
19   const sp_cookie *cookie = sp_lookup_cookie_config(hash_key->key);
20 
21   /* If the cookie isn't in the conf, it shouldn't be encrypted. */
22   if (!cookie || !cookie->encrypt) {
23     return ZEND_HASH_APPLY_KEEP;
24   }
25 
26   /* If the cookie has no value, it shouldn't be encrypted. */
27   if (0 == Z_STRLEN_P(pDest)) {
28     return ZEND_HASH_APPLY_KEEP;
29   }
30 
31   return decrypt_zval(pDest, cookie->simulation, hash_key);
32 }
33 
encrypt_data(zend_string * data)34 static zend_string *encrypt_data(zend_string *data) {
35   zend_string *z = encrypt_zval(data);
36   sp_log_debug("cookie_encryption", "Cookie value:%s:", ZSTR_VAL(z));
37   return z;
38 }
39 
40 #if PHP_VERSION_ID >= 70300
php_head_parse_cookie_options_array(zval * options,zend_long * expires,zend_string ** path,zend_string ** domain,zend_bool * secure,zend_bool * httponly,zend_string ** samesite)41 static void php_head_parse_cookie_options_array(
42     zval *options, zend_long *expires, zend_string **path, zend_string **domain,
43     zend_bool *secure, zend_bool *httponly, zend_string **samesite) {
44   int found = 0;
45   zend_string *key;
46   zval *value;
47 
48   ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), key, value) {
49     if (key) {
50       if (zend_string_equals_literal_ci(key, "expires")) {
51         *expires = zval_get_long(value);
52         found++;
53       } else if (zend_string_equals_literal_ci(key, "path")) {
54         *path = zval_get_string(value);
55         found++;
56       } else if (zend_string_equals_literal_ci(key, "domain")) {
57         *domain = zval_get_string(value);
58         found++;
59       } else if (zend_string_equals_literal_ci(key, "secure")) {
60         *secure = zval_is_true(value);
61         found++;
62       } else if (zend_string_equals_literal_ci(key, "httponly")) {
63         *httponly = zval_is_true(value);
64         found++;
65       } else if (zend_string_equals_literal_ci(key, "samesite")) {
66         *samesite = zval_get_string(value);
67         found++;
68       } else {
69         php_error_docref(NULL, E_WARNING,
70                          "Unrecognized key '%s' found in the options array",
71                          ZSTR_VAL(key));
72       }
73     } else {
74       php_error_docref(NULL, E_WARNING,
75                        "Numeric key found in the options array");
76     }
77   }
78   ZEND_HASH_FOREACH_END();
79 
80   /* Array is not empty but no valid keys were found */
81   if (found == 0 && zend_hash_num_elements(Z_ARRVAL_P(options)) > 0) {
82     php_error_docref(NULL, E_WARNING,
83                      "No valid options were found in the given array");
84   }
85 }
86 #endif
87 
PHP_FUNCTION(sp_setcookie)88 PHP_FUNCTION(sp_setcookie) {
89   zend_string *name = NULL, *value = NULL, *path = NULL, *domain = NULL,
90               *value_enc = NULL,
91 #if PHP_VERSION_ID < 70300
92               *path_samesite = NULL;
93 #else
94               *samesite = NULL;
95 #endif
96 
97   zend_long expires = 0;
98   zval *expires_or_options = NULL;
99   zend_bool secure = 0, httponly = 0;
100   const sp_cookie *cookie_node = NULL;
101   char *cookie_samesite;
102 
103   // LCOV_EXCL_BR_START
104   ZEND_PARSE_PARAMETERS_START(1, 7)
105   Z_PARAM_STR(name)
106   Z_PARAM_OPTIONAL
107   Z_PARAM_STR(value)
108   Z_PARAM_ZVAL(expires_or_options)
109   Z_PARAM_STR(path)
110   Z_PARAM_STR(domain)
111   Z_PARAM_BOOL(secure)
112   Z_PARAM_BOOL(httponly)
113   ZEND_PARSE_PARAMETERS_END();
114   // LCOV_EXCL_BR_END
115 
116   if (expires_or_options) {
117 #if PHP_VERSION_ID < 70300
118     expires = zval_get_long(expires_or_options);
119 #else
120     if (Z_TYPE_P(expires_or_options) == IS_ARRAY) {
121       if (UNEXPECTED(ZEND_NUM_ARGS() > 3)) {
122         php_error_docref(NULL, E_WARNING,
123                          "Cannot pass arguments after the options array");
124         RETURN_FALSE;
125       }
126       php_head_parse_cookie_options_array(expires_or_options, &expires, &path,
127                                           &domain, &secure, &httponly,
128                                           &samesite);
129     } else {
130       expires = zval_get_long(expires_or_options);
131     }
132 #endif
133   }
134 
135   /* If the request was issued over HTTPS, the cookie should be "secure" */
136   if (SNUFFLEUPAGUS_G(config).config_auto_cookie_secure) {
137     const zval server_vars = PG(http_globals)[TRACK_VARS_SERVER];
138     if (Z_TYPE(server_vars) == IS_ARRAY) {
139       const zval *is_https =
140           zend_hash_str_find(Z_ARRVAL(server_vars), "HTTPS", strlen("HTTPS"));
141       if (NULL != is_https) {
142         secure = 1;
143       }
144     }
145   }
146 
147   /* lookup existing configuration for said cookie */
148   cookie_node = sp_lookup_cookie_config(name);
149 
150   /* If the cookie's value is encrypted, it won't be usable by
151    * javascript anyway.
152    */
153   if (cookie_node && cookie_node->encrypt) {
154     httponly = 1;
155   }
156 
157   /* Shall we encrypt the cookie's value? */
158   if (cookie_node && cookie_node->encrypt && value) {
159     value_enc = encrypt_data(value);
160   }
161 
162   if (cookie_node && cookie_node->samesite) {
163     if (!path) {
164       path = zend_string_init("", 0, 0);
165     }
166 #if PHP_VERSION_ID < 70300
167     cookie_samesite = (cookie_node->samesite == lax)
168                           ? SAMESITE_COOKIE_FORMAT SP_TOKEN_SAMESITE_LAX
169                           : SAMESITE_COOKIE_FORMAT SP_TOKEN_SAMESITE_STRICT;
170 
171     /* Concatenating everything, as is in PHP internals */
172     path_samesite = zend_string_init(ZSTR_VAL(path), ZSTR_LEN(path), 0);
173     path_samesite = zend_string_extend(
174         path_samesite, ZSTR_LEN(path) + strlen(cookie_samesite) + 1, 0);
175     memcpy(ZSTR_VAL(path_samesite) + ZSTR_LEN(path), cookie_samesite,
176            strlen(cookie_samesite) + 1);
177 #else
178     cookie_samesite = (cookie_node->samesite == lax) ? SP_TOKEN_SAMESITE_LAX
179                                                      : SP_TOKEN_SAMESITE_STRICT;
180 
181     samesite = zend_string_init(cookie_samesite, strlen(cookie_samesite), 0);
182 #endif
183   }
184 
185 #if PHP_VERSION_ID < 70300
186   if (php_setcookie(name, (value_enc ? value_enc : value), expires,
187                     (path_samesite ? path_samesite : path), domain, secure, 1,
188                     httponly) == SUCCESS) {
189 #else
190   if (php_setcookie(name, (value_enc ? value_enc : value), expires, path,
191                     domain, secure, httponly, samesite, 1) == SUCCESS) {
192 #endif
193     RETVAL_TRUE;
194   } else {
195     RETVAL_FALSE;
196   }
197 
198   if (value_enc) {
199     zend_string_release(value_enc);
200   }
201 #if PHP_VERSION_ID < 70300
202   if (path_samesite) {
203     zend_string_release(path_samesite);
204   }
205 #endif
206 }
207 
208 int hook_cookies() {
209   HOOK_FUNCTION("setcookie", sp_internal_functions_hook, PHP_FN(sp_setcookie));
210 
211   return SUCCESS;
212 }
213