| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentForm\App\Modules\Form; |
| 4 | 4 | |
| 5 | +use FluentForm\App\Helpers\Helper; | |
| 5 | 6 | use FluentForm\Framework\Foundation\Application; |
| 6 | 7 | use FluentForm\Framework\Helpers\ArrayHelper; |
| 7 | 8 | |
| 8 | - | |
| 9 | 9 | class HoneyPot |
| 10 | 10 | { |
| 11 | 11 | private $app; |
| 12 | 12 | |
| @@ -19,29 +19,58 @@ | ||
| 19 | 19 | { |
| 20 | 20 | if (!$this->isEnabled($form->id)) { |
| 21 | 21 | return; |
| 22 | 22 | } |
| 23 | + | |
| 24 | + $fieldName = $this->getFieldName($form->id); | |
| 25 | + $fieldId = 'ff_' . $form->id . '_item_sf' ; | |
| 26 | + $labels = ['Newsletter', 'Updates', 'Contact', 'Subscribe', 'Notify']; | |
| 27 | + $randomLabel = $labels[array_rand($labels)]; | |
| 23 | 28 | ?> |
| 24 | - <span style="display: none !important;"><input type="checkbox" name="<?php echo $this->getFieldName($form->id); ?>" value="1" | |
| 25 | - style="display:none !important;" tabindex="-1"></span> | |
| 29 | + <div | |
| 30 | + style="display: none!important; position: absolute!important; transform: translateX(1000%)!important;" | |
| 31 | + class="ff-el-group ff-hpsf-container" | |
| 32 | + > | |
| 33 | + <div class="ff-el-input--label asterisk-right"> | |
| 34 | + <label for="<?php echo esc_attr($fieldId); ?>" aria-label="<?php echo esc_attr($randomLabel); ?>"> | |
| 35 | + <?php echo esc_html($randomLabel); ?> | |
| 36 | + </label> | |
| 37 | + </div> | |
| 38 | + <div class="ff-el-input--content"> | |
| 39 | + <input type="text" | |
| 40 | + name="<?php echo esc_attr($fieldName); ?>" | |
| 41 | + class="ff-el-form-control" | |
| 42 | + id="<?php echo esc_attr($fieldId); ?>" | |
| 43 | + /> | |
| 44 | + </div> | |
| 45 | + </div> | |
| 26 | 46 | <?php |
| 27 | 47 | } |
| 28 | 48 | |
| 29 | 49 | public function verify($insertData, $requestData, $formId) |
| 30 | 50 | { |
| 51 | + // SECURITY (FINDING-25): do NOT skip the check for conversational forms based on the | |
| 52 | + // client-supplied isFFConversational flag. The conversational renderer now injects the | |
| 53 | + // honeypot field (empty) into the submission (getConversationalHoneypotInput via | |
| 54 | + // extra_inputs), so the "present and empty" check passes for legitimate conversational | |
| 55 | + // submissions and the control can no longer be bypassed with a single flag. | |
| 31 | 56 | if (!$this->isEnabled($formId)) { |
| 32 | 57 | return; |
| 33 | 58 | } |
| 34 | 59 | |
| 35 | - // Now verify | |
| 36 | - if (ArrayHelper::get($requestData, $this->getFieldName($formId))) { | |
| 37 | - // It's a bot! Block him | |
| 38 | - wp_send_json( | |
| 39 | - array( | |
| 40 | - 'errors' => 'Sorry! You can not submit this form at this moment!' | |
| 41 | - ), 422); | |
| 60 | + $honeyPotName = $this->getFieldName($formId); | |
| 61 | + | |
| 62 | + if ( | |
| 63 | + !ArrayHelper::exists($requestData, $honeyPotName) || | |
| 64 | + !empty(ArrayHelper::get($requestData, $honeyPotName)) | |
| 65 | + ) { | |
| 66 | + $message = apply_filters( | |
| 67 | + 'fluentform/honeypot_spam_message', | |
| 68 | + __('Sorry! You can not submit this form at this moment!', 'fluentform'), | |
| 69 | + $formId | |
| 70 | + ); | |
| 71 | + wp_send_json(['errors' => $message], 422); | |
| 42 | 72 | } |
| 43 | - | |
| 44 | 73 | return; |
| 45 | 74 | } |
| 46 | 75 | |
| 47 | 76 | public function isEnabled($formId = false) |
| @@ -46,14 +75,33 @@ | ||
| 46 | 75 | |
| 47 | 76 | public function isEnabled($formId = false) |
| 48 | 77 | { |
| 49 | 78 | $option = get_option('_fluentform_global_form_settings'); |
| 50 | - $status = ArrayHelper::get($option, 'misc.honeypotStatus') == 'yes'; | |
| 51 | - return apply_filters('fluentform_honeypot_status', $status, $formId); | |
| 79 | + $status = 'yes' == ArrayHelper::get($option, 'misc.honeypotStatus'); | |
| 80 | + return apply_filters('fluentform/honeypot_status', $status, $formId); | |
| 52 | 81 | } |
| 53 | 82 | |
| 83 | + /** | |
| 84 | + * SECURITY (FINDING-25): the conversational form is a JS app that never renders the DOM | |
| 85 | + * honeypot field, so verify() previously had to be skipped for it (via the client-controlled | |
| 86 | + * isFFConversational flag). Return the honeypot field pre-filled EMPTY so the conversational | |
| 87 | + * JS carries it in the submission and the "present and empty" check passes for a legitimate | |
| 88 | + * submission while the control is enforced server-side rather than bypassable by a flag. | |
| 89 | + * | |
| 90 | + * @param int $formId | |
| 91 | + * @return array | |
| 92 | + */ | |
| 93 | + public function getConversationalHoneypotInput($formId) | |
| 94 | + { | |
| 95 | + if (!$this->isEnabled($formId)) { | |
| 96 | + return []; | |
| 97 | + } | |
| 98 | + | |
| 99 | + return [$this->getFieldName($formId) => '']; | |
| 100 | + } | |
| 101 | + | |
| 54 | 102 | private function getFieldName($formId) |
| 55 | 103 | { |
| 56 | - return apply_filters('fluentform_honeypot_name', 'item__' . $formId . '__fluent_checkme_', $formId); | |
| 104 | + $honeyPotName = 'item_' . $formId . '__fluent_sf'; | |
| 105 | + return apply_filters('fluentform/honeypot_name', $honeyPotName, $formId); | |
| 57 | 106 | } |
| 58 | - | |
| 59 | -} | |
| 107 | +} | |