| @@ -10,8 +10,10 @@ | ||
| 10 | 10 | use FluentForm\App\Helpers\Helper; |
| 11 | 11 | |
| 12 | 12 | class ShortCodeParser |
| 13 | 13 | { |
| 14 | + const USER_SECRET_PROPERTIES = ['user_pass', 'user_activation_key', 'session_tokens', 'data']; | |
| 15 | + | |
| 14 | 16 | protected static $form = null; |
| 15 | 17 | |
| 16 | 18 | protected static $entry = null; |
| 17 | 19 | |
| @@ -95,9 +97,9 @@ | ||
| 95 | 97 | 'ff_will_return_html', |
| 96 | 98 | [ |
| 97 | 99 | false, |
| 98 | 100 | $provider, |
| 99 | - $key | |
| 101 | + $key, | |
| 100 | 102 | ], |
| 101 | 103 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 102 | 104 | 'fluentform/will_return_html', |
| 103 | 105 | 'Use fluentform/will_return_html instead of ff_will_return_html.' |
| @@ -106,9 +108,9 @@ | ||
| 106 | 108 | } |
| 107 | 109 | $parsable[$key] = static::parseShortCodeFromString($value, $isUrl, $isHtml, $htmlSanitized); |
| 108 | 110 | } |
| 109 | 111 | } |
| 110 | - | |
| 112 | + | |
| 111 | 113 | return $parsable; |
| 112 | 114 | } |
| 113 | 115 | |
| 114 | 116 | protected static function parseShortCodeFromString($parsable, $isUrl = false, $isHtml = false, $htmlSanitized = false) |
| @@ -124,9 +126,9 @@ | ||
| 124 | 126 | $value = ''; |
| 125 | 127 | if (false !== strpos($matches[1], 'inputs.')) { |
| 126 | 128 | $formProperty = substr($matches[1], strlen('inputs.')); |
| 127 | 129 | $value = static::getFormData($formProperty, $isHtml); |
| 128 | - } else if (false !== strpos($matches[1], 'labels.')) { | |
| 130 | + } elseif (false !== strpos($matches[1], 'labels.')) { | |
| 129 | 131 | $formLabelProperty = substr($matches[1], strlen('labels.')); |
| 130 | 132 | $value = static::getFormLabelData($formLabelProperty); |
| 131 | 133 | } elseif (false !== strpos($matches[1], 'user.')) { |
| 132 | 134 | $userProperty = substr($matches[1], strlen('user.')); |
| @@ -141,17 +143,17 @@ | ||
| 141 | 143 | $submissionProperty = substr($matches[1], strlen('submission.')); |
| 142 | 144 | $value = static::getSubmissionData($submissionProperty); |
| 143 | 145 | } elseif (false !== strpos($matches[1], 'cookie.')) { |
| 144 | 146 | $scookieProperty = substr($matches[1], strlen('cookie.')); |
| 145 | - $value = array_key_exists($scookieProperty, $_COOKIE) ? wp_unslash($_COOKIE[$scookieProperty]) : ''; | |
| 147 | + $value = array_key_exists($scookieProperty, $_COOKIE) ? sanitize_text_field(wp_unslash($_COOKIE[$scookieProperty])) : ''; | |
| 146 | 148 | } elseif (false !== strpos($matches[1], 'payment.')) { |
| 147 | 149 | $property = substr($matches[1], strlen('payment.')); |
| 148 | 150 | $deprecatedValue = apply_filters_deprecated( |
| 149 | 151 | 'fluentform_payment_smartcode', [ |
| 150 | - '', | |
| 151 | - $property, | |
| 152 | - self::getInstance() | |
| 153 | - ], | |
| 152 | + '', | |
| 153 | + $property, | |
| 154 | + self::getInstance(), | |
| 155 | + ], | |
| 154 | 156 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 155 | 157 | 'fluentform/payment_smartcode', |
| 156 | 158 | 'Use fluentform/payment_smartcode instead of fluentform_payment_smartcode.' |
| 157 | 159 | ); |
| @@ -165,10 +167,13 @@ | ||
| 165 | 167 | $value = fluentImplodeRecursive(', ', $value); |
| 166 | 168 | } |
| 167 | 169 | |
| 168 | 170 | if ($isUrl) { |
| 169 | - $value = rawurlencode($value); | |
| 170 | - } else if ($htmlSanitized) { | |
| 171 | + // Don't encode values that are already complete URLs like {wp.site_url} | |
| 172 | + if (!preg_match('#^https?://#i', (string) $value)) { | |
| 173 | + $value = rawurlencode($value); | |
| 174 | + } | |
| 175 | + } elseif ($htmlSanitized) { | |
| 171 | 176 | $value = fluentform_sanitize_html($value); |
| 172 | 177 | } |
| 173 | 178 | |
| 174 | 179 | return $value; |
| @@ -223,9 +228,9 @@ | ||
| 223 | 228 | [ |
| 224 | 229 | $originalInput, |
| 225 | 230 | $field, |
| 226 | 231 | static::getForm()->id, |
| 227 | - $isHtml | |
| 232 | + $isHtml, | |
| 228 | 233 | ], |
| 229 | 234 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 230 | 235 | 'fluentform/response_render_' . $field['element'], |
| 231 | 236 | 'Use fluentform/response_render_' . $field['element'] . ' instead of fluentform_response_render_' . $field['element'] |
| @@ -244,9 +249,9 @@ | ||
| 244 | 249 | [ |
| 245 | 250 | static::$store['inputs'][$key], |
| 246 | 251 | $field, |
| 247 | 252 | static::getForm()->id, |
| 248 | - $isHtml | |
| 253 | + $isHtml, | |
| 249 | 254 | ], |
| 250 | 255 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 251 | 256 | 'fluentform/response_render_' . $field['element'], |
| 252 | 257 | 'Use fluentform/response_render_' . $field['element'] . ' instead of fluentform_response_render_' . $field['element'] |
| @@ -277,9 +282,9 @@ | ||
| 277 | 282 | $key = $currentFieldName; |
| 278 | 283 | } |
| 279 | 284 | $inputLabel = ArrayHelper::get(ArrayHelper::get(static::$formFields, $key, []), 'label', ''); |
| 280 | 285 | $inputLabel = str_replace(['[', ']'], '', $inputLabel); |
| 281 | - $keys = explode(".", $key); | |
| 286 | + $keys = explode('.', $key); | |
| 282 | 287 | if (count($keys) > 1) { |
| 283 | 288 | $parentKey = array_shift($keys); |
| 284 | 289 | $inputLabel = str_replace($parentKey, '', $inputLabel); |
| 285 | 290 | } |
| @@ -298,9 +303,34 @@ | ||
| 298 | 303 | { |
| 299 | 304 | if (is_null(static::$store['user'])) { |
| 300 | 305 | static::$store['user'] = wp_get_current_user(); |
| 301 | 306 | } |
| 302 | - return static::$store['user']->{$key}; | |
| 307 | + | |
| 308 | + $user = static::$store['user']; | |
| 309 | + | |
| 310 | + // SECURITY (FINDING-11): `$user->{$key}` reads straight from the wp_users row via | |
| 311 | + // WP_User::__get, so an author-controlled {user.user_pass} (or {user.user_activation_key}) | |
| 312 | + // would exfiltrate the *submitting* user's password hash / reset token in a notification. | |
| 313 | + // Allow only a fixed set of safe profile fields; resolve anything else from user meta, | |
| 314 | + // which never contains the sensitive wp_users columns. | |
| 315 | + $allowed = [ | |
| 316 | + 'ID', 'id', 'display_name', 'first_name', 'last_name', 'user_email', | |
| 317 | + 'user_login', 'user_nicename', 'nickname', 'user_url', 'description', 'roles', | |
| 318 | + 'user_registered', // non-sensitive wp_users column; keep {user.user_registered} working | |
| 319 | + ]; | |
| 320 | + if (static::isDeniedUserProperty($key)) { | |
| 321 | + return ''; | |
| 322 | + } | |
| 323 | + if (in_array($key, $allowed, true)) { | |
| 324 | + return $user->{$key}; | |
| 325 | + } | |
| 326 | + | |
| 327 | + $key = (string) $key; | |
| 328 | + if ($user->ID && '' !== $key) { | |
| 329 | + return get_user_meta($user->ID, $key, true); | |
| 330 | + } | |
| 331 | + | |
| 332 | + return ''; | |
| 303 | 333 | } |
| 304 | 334 | |
| 305 | 335 | protected static function getPostData($key) |
| 306 | 336 | { |
| @@ -315,9 +345,9 @@ | ||
| 315 | 345 | |
| 316 | 346 | if (false !== strpos($key, 'author.')) { |
| 317 | 347 | $authorProperty = substr($key, strlen('author.')); |
| 318 | 348 | $authorId = static::$store['post']->post_author; |
| 319 | - if ($authorId) { | |
| 349 | + if ($authorId && !static::isDeniedUserProperty($authorProperty)) { | |
| 320 | 350 | $data = get_the_author_meta($authorProperty, $authorId); |
| 321 | 351 | if (!is_array($data)) { |
| 322 | 352 | return $data; |
| 323 | 353 | } |
| @@ -342,11 +372,30 @@ | ||
| 342 | 372 | return ''; |
| 343 | 373 | } |
| 344 | 374 | } |
| 345 | 375 | |
| 376 | + if ('post_password' === $key) { | |
| 377 | + return ''; | |
| 378 | + } | |
| 379 | + | |
| 346 | 380 | return static::$store['post']->{$key}; |
| 347 | 381 | } |
| 348 | 382 | |
| 383 | + // Shared by {user.*} and {embed_post.author.*} in both parsers. get_the_author_meta() and | |
| 384 | + // WP_User fall through to any user meta, where plugins keep 2FA secrets and tokens under | |
| 385 | + // protected (underscore) keys, so the secret columns alone are not enough to deny. | |
| 386 | + public static function isDeniedUserProperty($property) | |
| 387 | + { | |
| 388 | + // Same aliases get_the_author_meta() accepts: 'pass' means user_pass | |
| 389 | + if (in_array($property, ['login', 'pass', 'nicename', 'email', 'url', 'registered', 'activation_key', 'status'], true)) { | |
| 390 | + $property = 'user_' . $property; | |
| 391 | + } | |
| 392 | + | |
| 393 | + $denied = (array) apply_filters('fluentform/smartcode_user_denied_properties', self::USER_SECRET_PROPERTIES); | |
| 394 | + | |
| 395 | + return in_array($property, $denied, true) || is_protected_meta($property, 'user'); | |
| 396 | + } | |
| 397 | + | |
| 349 | 398 | protected static function getWPData($key) |
| 350 | 399 | { |
| 351 | 400 | if ('admin_email' == $key) { |
| 352 | 401 | return get_option('admin_email'); |
| @@ -367,9 +416,11 @@ | ||
| 367 | 416 | if (empty($entry->id)) { |
| 368 | 417 | return ''; |
| 369 | 418 | } |
| 370 | 419 | |
| 371 | - if (property_exists($entry, $key)) { | |
| 420 | + $columns = Helper::getEntryColumns($entry); | |
| 421 | + | |
| 422 | + if (array_key_exists($key, $columns)) { | |
| 372 | 423 | if ('total_paid' == $key || 'payment_total' == $key) { |
| 373 | 424 | return round($entry->{$key} / 100, 2); |
| 374 | 425 | } |
| 375 | 426 | if ('payment_method' == $key && 'test' == $entry->{$key}) { |
| @@ -415,9 +466,9 @@ | ||
| 415 | 466 | |
| 416 | 467 | $status = apply_filters_deprecated( |
| 417 | 468 | 'fluentform_all_data_skip_password_field', |
| 418 | 469 | [ |
| 419 | - __return_true() | |
| 470 | + __return_true(), | |
| 420 | 471 | ], |
| 421 | 472 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 422 | 473 | 'fluentform/all_data_skip_password_field', |
| 423 | 474 | 'Use fluentform/all_data_skip_password_field instead of fluentform_all_data_skip_password_field.' |
| @@ -435,9 +486,9 @@ | ||
| 435 | 486 | $hideHiddenField = true; |
| 436 | 487 | $hideHiddenField = apply_filters_deprecated( |
| 437 | 488 | 'fluentform_all_data_without_hidden_fields', |
| 438 | 489 | [ |
| 439 | - $hideHiddenField | |
| 490 | + $hideHiddenField, | |
| 440 | 491 | ], |
| 441 | 492 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 442 | 493 | 'fluentform/all_data_without_hidden_fields', |
| 443 | 494 | 'Use fluentform/all_data_without_hidden_fields instead of fluentform_all_data_without_hidden_fields.' |
| @@ -470,9 +521,9 @@ | ||
| 470 | 521 | [ |
| 471 | 522 | $html, |
| 472 | 523 | $formFields, |
| 473 | 524 | $inputLabels, |
| 474 | - $response | |
| 525 | + $response, | |
| 475 | 526 | ], |
| 476 | 527 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 477 | 528 | 'fluentform/all_data_shortcode_html', |
| 478 | 529 | 'Use fluentform/all_data_shortcode_html instead of fluentform_all_data_shortcode_html.' |
| @@ -483,9 +534,9 @@ | ||
| 483 | 534 | } elseif (0 === strpos($key, 'pdf.download_link.')) { |
| 484 | 535 | $key = apply_filters_deprecated( |
| 485 | 536 | 'fluentform_shortcode_parser_callback_pdf.download_link.public', |
| 486 | 537 | [ |
| 487 | - $key, static::getInstance() | |
| 538 | + $key, static::getInstance(), | |
| 488 | 539 | ], |
| 489 | 540 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 490 | 541 | 'fluentform/shortcode_parser_callback_pdf.download_link.public', |
| 491 | 542 | 'Use fluentform/shortcode_parser_callback_pdf.download_link.public instead of fluentform_shortcode_parser_callback_pdf.download_link.public.' |
| @@ -523,15 +574,13 @@ | ||
| 523 | 574 | } |
| 524 | 575 | return ''; |
| 525 | 576 | } |
| 526 | 577 | |
| 527 | - | |
| 528 | 578 | // if it's multi line then just return |
| 529 | 579 | if (false !== strpos($key, PHP_EOL)) { // most probably it's a css |
| 530 | 580 | return '{' . $key . '}'; |
| 531 | 581 | } |
| 532 | 582 | |
| 533 | - | |
| 534 | 583 | $groups = explode('.', $key); |
| 535 | 584 | if (count($groups) > 1) { |
| 536 | 585 | $group = array_shift($groups); |
| 537 | 586 | $property = implode('.', $groups); |
| @@ -538,9 +587,9 @@ | ||
| 538 | 587 | $handlerValue = apply_filters_deprecated( |
| 539 | 588 | 'fluentform_smartcode_group_' . $group, |
| 540 | 589 | [ |
| 541 | 590 | $property, |
| 542 | - static::getInstance() | |
| 591 | + static::getInstance(), | |
| 543 | 592 | ], |
| 544 | 593 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 545 | 594 | 'fluentform/smartcode_group_' . $group, |
| 546 | 595 | 'Use fluentform/smartcode_group_' . $group . ' instead of fluentform_smartcode_group_' . $group |
| @@ -556,15 +605,14 @@ | ||
| 556 | 605 | $handlerValue = apply_filters_deprecated( |
| 557 | 606 | 'fluentform_shortcode_parser_callback_' . $key, |
| 558 | 607 | [ |
| 559 | 608 | '{' . $key . '}', |
| 560 | - static::getInstance() | |
| 609 | + static::getInstance(), | |
| 561 | 610 | ], |
| 562 | 611 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 563 | 612 | 'fluentform/shortcode_parser_callback_' . $key, |
| 564 | 613 | 'Use fluentform/shortcode_parser_callback_' . $key . ' instead of fluentform_shortcode_parser_callback_' . $key |
| 565 | 614 | ); |
| 566 | - | |
| 567 | 615 | |
| 568 | 616 | $handlerValue = apply_filters('fluentform/shortcode_parser_callback_' . $key, $handlerValue, static::getInstance()); |
| 569 | 617 | |
| 570 | 618 | if ($handlerValue) { |