PluginProbe ʕ •ᴥ•ʔ
JetFormBuilder — Dynamic Blocks Form Builder / 3.6.5.2
JetFormBuilder — Dynamic Blocks Form Builder v3.6.5.2
3.6.5.2 3.6.5.1 3.6.5 3.6.4.2 3.6.4.1 3.6.4 3.6.3.1 3.6.3 3.6.2.2 3.6.2.1 3.6.2 3.6.1.1 3.6.1 3.6.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.1.0 2.1.1 2.1.10 2.1.11 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 3.0.0 3.0.0.1 3.0.0.2 3.0.0.3 3.0.1 3.0.1.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.0.1 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.3.1 3.3.4 3.3.4.1 3.3.4.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.5.1 3.4.5.2 3.4.6 3.4.7 3.4.7.1 3.5.0 3.5.1 3.5.1.1 3.5.1.2 3.5.2 3.5.2.1 3.5.3 3.5.4 3.5.5 3.5.6 3.5.6.1 3.5.6.2 3.5.6.3 3.6.0
jetformbuilder / modules / validation / advanced-rules / server-side-rule.php
jetformbuilder / modules / validation / advanced-rules Last commit date
interfaces 2 years ago traits 2 years ago match-not-regexp-rule.php 2 years ago match-regexp-rule.php 2 years ago must-contain-characters-rule.php 2 years ago must-equal-rule.php 2 years ago must-not-contain-characters-rule.php 2 years ago rule.php 2 years ago server-side-rule.php 5 days ago ssr-callback-allowlist.php 5 days ago
server-side-rule.php
398 lines
1 <?php
2
3
4 namespace JFB_Modules\Validation\Advanced_Rules;
5
6 use JFB_Modules\Validation\Ssr;
7 use Jet_Form_Builder\Exceptions\Repository_Exception;
8 use JFB_Components\Repository\Repository_Pattern_Trait;
9 use JFB_Modules\Block_Parsers\Field_Data_Parser;
10 use JFB_Modules\Validation\Handlers\Validation_Handler;
11 use Jet_Form_Builder\Request\Request_Tools;
12
13 // If this file is called directly, abort.
14 if ( ! defined( 'WPINC' ) ) {
15 die;
16 }
17
18 class Server_Side_Rule extends Rule {
19
20 use Repository_Pattern_Trait;
21
22 /**
23 * Blacklist of dangerous functions that should never be allowed as callbacks.
24 * All values MUST be lowercase for case-insensitive comparison.
25 *
26 * @since 3.5.6.2 Security fix: expanded list and case-insensitive check
27 */
28 const NOT_ALLOWED = array(
29 // Debug/output functions
30 'var_dump',
31 'var_export',
32 'print_r',
33 'sprintf',
34 'printf',
35 // Command execution
36 'shell_exec',
37 'system',
38 'exec',
39 'passthru',
40 'proc_open',
41 'popen',
42 'pcntl_exec',
43 'proc_nice',
44 'proc_terminate',
45 'proc_close',
46 // Code execution
47 'eval',
48 'assert',
49 'create_function',
50 'call_user_func',
51 'call_user_func_array',
52 'preg_replace_callback',
53 'array_map',
54 'array_filter',
55 'array_reduce',
56 'usort',
57 'uasort',
58 'uksort',
59 'array_walk',
60 'array_walk_recursive',
61 // User mutations
62 'wp_insert_user',
63 'wp_update_user',
64 // File inclusion
65 'include',
66 'include_once',
67 'require',
68 'require_once',
69 // Serialization (object injection)
70 'unserialize',
71 'maybe_unserialize',
72 // File operations
73 'file_get_contents',
74 'file_put_contents',
75 'fwrite',
76 'fputs',
77 'fopen',
78 'readfile',
79 'file',
80 'fread',
81 'fgets',
82 'fgetc',
83 'fgetcsv',
84 'fpassthru',
85 'move_uploaded_file',
86 'copy',
87 'rename',
88 'unlink',
89 'rmdir',
90 'mkdir',
91 'chmod',
92 'chown',
93 'chgrp',
94 // Network functions
95 'curl_exec',
96 'curl_multi_exec',
97 'fsockopen',
98 'pfsockopen',
99 'stream_socket_client',
100 'stream_socket_server',
101 // Dangerous PHP functions
102 'parse_str',
103 'extract',
104 'putenv',
105 'ini_set',
106 'ini_alter',
107 'dl',
108 'mail',
109 'header',
110 'setcookie',
111 'setrawcookie',
112 // POSIX functions
113 'posix_kill',
114 'posix_mkfifo',
115 'posix_setpgid',
116 'posix_setsid',
117 'posix_setuid',
118 'posix_setgid',
119 'posix_seteuid',
120 'posix_setegid',
121 // Apache functions
122 'apache_child_terminate',
123 'apache_setenv',
124 // Reflection/class manipulation
125 'get_defined_functions',
126 'get_defined_vars',
127 'get_defined_constants',
128 'phpinfo',
129 'highlight_file',
130 'show_source',
131 'php_strip_whitespace',
132 'get_cfg_var',
133 'get_current_user',
134 'getmyuid',
135 'getmypid',
136 'getenv',
137 // WordPress data/state mutation (added 3.6.5.3 — not blocked by the allowlist
138 // alone, since the allowlist only records what an editor once typed, not what
139 // is actually safe to expose to an unauthenticated caller's argument).
140 'wp_delete_file',
141 'wp_delete_post',
142 'wp_delete_attachment',
143 'wp_trash_post',
144 'delete_option',
145 'delete_site_option',
146 'delete_post_meta',
147 'delete_user_meta',
148 'delete_metadata',
149 'update_option',
150 'add_option',
151 'update_site_option',
152 'update_post_meta',
153 'update_user_meta',
154 'wp_set_password',
155 'wp_delete_user',
156 'add_user_meta',
157 'add_role',
158 'remove_role',
159 'wp_update_post',
160 'wp_insert_post',
161 'do_shortcode',
162 'do_action',
163 'apply_filters',
164 'deactivate_plugins',
165 'activate_plugin',
166 'switch_to_blog',
167 'restore_current_blog',
168 'wp_remote_get',
169 'wp_remote_post',
170 'wp_remote_request',
171 'wp_safe_remote_get',
172 'wp_safe_remote_post',
173 'wp_safe_remote_request',
174 'wp_remote_head',
175 'delete_transient',
176 'delete_site_transient',
177 'set_transient',
178 'set_site_transient',
179 'wp_cache_add',
180 'wp_cache_set',
181 'wp_cache_replace',
182 'wp_cache_delete',
183 'wp_cache_flush',
184 'wp_cache_flush_group',
185 'wp_cache_flush_runtime',
186 'wp_cache_incr',
187 'wp_cache_decr',
188 'clean_post_cache',
189 'clean_term_cache',
190 'clean_comment_cache',
191 'flush_rewrite_rules',
192 'wp_delete_comment',
193 'wp_trash_comment',
194 'wp_delete_term',
195 'wp_remove_object_terms',
196 'wp_die',
197 );
198
199 /**
200 * Function names that ship with the plugin as selectable "Server-Side callback"
201 * options and are safe by construction — they never reach `call_user_func()`,
202 * `validate()` resolves them straight to a `Ssr\Base_Validation_Callback` instance.
203 * Kept separate from the allowlist option so a fresh install already offers them.
204 *
205 * @since 3.6.5.2
206 */
207 const BUILTIN_ALLOWED = array();
208
209 public function __construct() {
210 $this->rep_install();
211 }
212
213 /**
214 * Whether $function_name resolves to one of the built-in callbacks (registered via
215 * `rep_instances()`/`get_id()`), i.e. it will never be passed to `call_user_func()`.
216 *
217 * @since 3.6.5.2
218 */
219 public static function is_builtin_callback( string $function_name ): bool {
220 foreach ( ( new self() )->rep_instances() as $callback ) {
221 if ( $callback->get_id() === $function_name ) {
222 return true;
223 }
224 }
225
226 return false;
227 }
228
229 public function rep_instances(): array {
230 return apply_filters(
231 'jet-form-builder/validation-callbacks',
232 array(
233 new Ssr\Is_User_Login_Unique(),
234 new Ssr\Is_User_Email_Unique(),
235 new Ssr\Is_Field_Value_Unique(),
236 new Ssr\Is_User_Password_Valid(),
237 )
238 );
239 }
240
241 public function get_id(): string {
242 return 'ssr';
243 }
244
245 public function get_label(): string {
246 return __( 'Server-Side callback', 'jet-form-builder' );
247 }
248
249 public function validate_field( Field_Data_Parser $parser ) {
250 $function_name = $this->get_setting( 'value' );
251
252 if ( ! $this->validate_submission_signature( $parser ) ) {
253 $parser->collect_error( 'rule:ssr:invalid_signature', $this->get_setting( 'message' ) );
254
255 return;
256 }
257
258 $is_valid = $this->validate( $parser, $function_name );
259
260 if ( $is_valid ) {
261 return;
262 }
263
264 $parser->collect_error( 'rule:ssr:' . $function_name, $this->get_setting( 'message' ) );
265 }
266
267 /**
268 * @return Ssr\Base_Validation_Callback[]
269 */
270 public function get_callbacks(): array {
271 return $this->rep_get_items();
272 }
273
274 protected function validate( Field_Data_Parser $parser, string $function_name ): bool {
275 try {
276 /** @var Ssr\Base_Validation_Callback $callback */
277 $callback = $this->rep_get_item( $function_name );
278 } catch ( Repository_Exception $exception ) {
279 return $this->validate_custom( $parser, $function_name );
280 }
281
282 return $callback->is_valid_with_parser( $parser );
283 }
284
285 protected function validate_submission_signature( Field_Data_Parser $parser ): bool {
286 $rule_index = $this->get_setting( '_rule_index' );
287
288 if ( false === $rule_index ) {
289 return true;
290 }
291
292 return Validation_Handler::validate_main_signature(
293 Request_Tools::get_request(),
294 jet_fb_handler()->get_form_id(),
295 explode( '.', $parser->get_scoped_name() ),
296 absint( $rule_index )
297 );
298 }
299
300 protected function validate_custom( Field_Data_Parser $parser, string $function_name ): bool {
301 $name = $this->validate_callback( $function_name );
302
303 if ( ! $name ) {
304 return false;
305 }
306
307 return (bool) call_user_func( $name, $parser->get_value(), $parser->get_context() );
308 }
309
310 /**
311 * Validate callback function name for security.
312 *
313 * Two checks apply, both must pass:
314 * 1. Denylist (`NOT_ALLOWED`) — kept as defense in depth, blocks known-catastrophic
315 * functions outright even if the allowlist below is ever misconfigured.
316 * 2. Allowlist — a function must be explicitly known-safe: either shipped with the
317 * plugin, collected from a form an editor actually saved with this callback
318 * configured (`Ssr_Callback_Allowlist`), or added by a site via the
319 * `jet-form-builder/ssr-validation/allowed-callbacks` filter. A denylist alone
320 * cannot enumerate every dangerous function in PHP core + WordPress core + active
321 * plugins, so functions unknown to either list are rejected by default.
322 *
323 * @since 3.5.6.2 Denylist introduced.
324 * @since 3.6.5.2 Allowlist enforcement added.
325 *
326 * @param string $function_name The function name to validate.
327 *
328 * @return string Empty string if invalid, function name if valid.
329 */
330 protected function validate_callback( string $function_name ): string {
331 $name = preg_replace( '/[^\w]/i', '', $function_name );
332
333 if ( $name !== $function_name ) {
334 return '';
335 }
336
337 // Case-insensitive checks (PHP function names are case-insensitive).
338 $name_lower = strtolower( $name );
339
340 if ( in_array( $name_lower, self::NOT_ALLOWED, true ) ) {
341 return '';
342 }
343
344 if ( ! function_exists( $name ) ) {
345 return '';
346 }
347
348 $allowed = $this->get_allowed_callbacks();
349
350 if ( ! in_array( $name_lower, $allowed, true ) ) {
351 Ssr_Callback_Allowlist::refresh_allowed_callbacks_for_form(
352 (int) jet_fb_handler()->get_form_id()
353 );
354 $allowed = $this->get_allowed_callbacks();
355 }
356
357 return in_array( $name_lower, $allowed, true ) ? $name : '';
358 }
359
360 /**
361 * Scoped to the form actually being submitted — a function name configured on one
362 * form is never usable from another form, even though both may have been saved by
363 * the same (already admin-gated) capability. See `Ssr_Callback_Allowlist` for why
364 * this matters: the allowlist only records "some editor typed this name once", not
365 * "this function is safe everywhere", so it must not widen past the form an editor
366 * actually approved it on.
367 *
368 * @since 3.6.5.2
369 * @since 3.6.5.3 Scoped per-form instead of site-wide.
370 *
371 * @return string[] Lowercased function names allowed to run via `call_user_func()`.
372 */
373 protected function get_allowed_callbacks(): array {
374 $allowed = array_merge(
375 self::BUILTIN_ALLOWED,
376 Ssr_Callback_Allowlist::get_allowed_callbacks_for_form(
377 (int) jet_fb_handler()->get_form_id()
378 )
379 );
380
381 $allowed = (array) apply_filters(
382 'jet-form-builder/ssr-validation/allowed-callbacks',
383 array_values( array_unique( array_map( 'strtolower', $allowed ) ) ),
384 (int) jet_fb_handler()->get_form_id()
385 );
386
387 $allowed = array_map(
388 static function ( $callback ): string {
389 return is_string( $callback ) ? strtolower( $callback ) : '';
390 },
391 $allowed
392 );
393
394 return array_values( array_unique( array_filter( $allowed ) ) );
395 }
396
397 }
398