| @@ -64,24 +64,51 @@ | ||
| 64 | 64 | { |
| 65 | 65 | $timeStamp = current_time('timestamp'); |
| 66 | 66 | $fieldName = $this->getFieldName($formId); |
| 67 | 67 | $data = implode('|', [$timeStamp, $formId, $fieldName]); |
| 68 | - | |
| 68 | + | |
| 69 | 69 | return apply_filters('fluentform/generated_protection_token', Protector::encrypt($data), $formId, $timeStamp); |
| 70 | 70 | } |
| 71 | + | |
| 72 | + /** | |
| 73 | + * SECURITY (FINDING-25): mint a token input for the conversational form so it is carried in the | |
| 74 | + * submission (the conversational JS forwards every extra_input) and the token check can be | |
| 75 | + * enforced server-side instead of being skippable via the client isFFConversational flag. This | |
| 76 | + * is static and self-contained to avoid the constructor's action registrations; it mirrors | |
| 77 | + * isEnabled() / getFieldName() / generateToken() above (same filters, same payload format, so | |
| 78 | + * validateToken() accepts it). | |
| 79 | + * | |
| 80 | + * @param int $formId | |
| 81 | + * @return array | |
| 82 | + */ | |
| 83 | + public static function getConversationalTokenInput($formId) | |
| 84 | + { | |
| 85 | + $option = get_option('_fluentform_global_form_settings'); | |
| 86 | + $enabled = 'yes' === Arr::get($option, 'misc.tokenBasedProtectionStatus'); | |
| 87 | + $enabled = apply_filters('fluentform/token_based_spam_protection_status', $enabled, $formId); | |
| 88 | + if (!$enabled) { | |
| 89 | + return []; | |
| 90 | + } | |
| 91 | + | |
| 92 | + $fieldName = apply_filters('fluentform/token_protection_name', '__fluent_protection_token_' . $formId, $formId); | |
| 93 | + $timeStamp = current_time('timestamp'); | |
| 94 | + $data = implode('|', [$timeStamp, $formId, $fieldName]); | |
| 95 | + $token = apply_filters('fluentform/generated_protection_token', Protector::encrypt($data), $formId, $timeStamp); | |
| 96 | + | |
| 97 | + return [$fieldName => $token]; | |
| 98 | + } | |
| 71 | 99 | |
| 72 | 100 | public function verify($insertData, $requestData, $formId) |
| 73 | 101 | { |
| 74 | - if ( | |
| 75 | - !$this->isEnabled($formId) || | |
| 76 | - ( | |
| 77 | - Helper::isConversionForm($formId) && | |
| 78 | - Arr::isTrue($requestData, 'isFFConversational') | |
| 79 | - ) | |
| 80 | - ) { | |
| 102 | + // SECURITY (FINDING-25): do NOT skip the check for conversational forms based on the | |
| 103 | + // client-supplied isFFConversational flag — that let an attacker bypass token protection by | |
| 104 | + // adding one parameter. The conversational renderer now injects a valid token into the | |
| 105 | + // submission (getConversationalTokenInput via extra_inputs), so the check is enforced for | |
| 106 | + // conversational and regular forms alike; only a genuinely-disabled feature is skipped. | |
| 107 | + if (!$this->isEnabled($formId)) { | |
| 81 | 108 | return; |
| 82 | 109 | } |
| 83 | - | |
| 110 | + | |
| 84 | 111 | $fieldName = $this->getFieldName($formId); |
| 85 | 112 | $token = sanitize_text_field(Arr::get($requestData, $fieldName)); |
| 86 | 113 | if (!$token || !$this->validateToken($token, $formId)) { |
| 87 | 114 | $errorMessage = apply_filters( |
| @@ -120,8 +147,16 @@ | ||
| 120 | 147 | return false; |
| 121 | 148 | } |
| 122 | 149 | |
| 123 | 150 | $isValid = (int)$tokenFormId === $formId && $fieldName === $this->getFieldName($formId); |
| 151 | + | |
| 152 | + // NOTE (FINDING-27): a per-token single-use cap was intentionally NOT implemented here. | |
| 153 | + // Storing one WordPress transient per minted token would create unbounded options-table | |
| 154 | + // writes on this high-frequency public path (tokens are cheaply minted via the public | |
| 155 | + // endpoint and expired transients are not proactively cleaned). The token remains a | |
| 156 | + // defence-in-depth anti-bot control (a bot must fetch a nonce-gated, form/field-bound, | |
| 157 | + // time-limited token); replay within the expiration window is the accepted LOW residual. | |
| 158 | + | |
| 124 | 159 | return apply_filters('fluentform/token_based_validation_result', |
| 125 | 160 | $isValid, |
| 126 | 161 | $timestamp, |
| 127 | 162 | $tokenFormId, |