PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.15
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.15
6.2.15 6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 All 197 releases
← All changes | app/Services/Submission/SubmissionService.php +68 -11 6.2.8 → 6.2.15 View file →
@@ -15,8 +15,9 @@
15 15 use FluentForm\Framework\Support\Collection;
16 16 use FluentForm\App\Services\Form\FormService;
17 17 use FluentForm\App\Modules\Form\FormDataParser;
18 18 use FluentForm\App\Modules\Form\FormFieldsParser;
19 +use FluentForm\App\Services\Manager\FormManagerService;
19 20
20 21 class SubmissionService
21 22 {
22 23 /**
@@ -190,8 +191,19 @@
190 191
191 192 $formId = Arr::get($attributes, 'form_id');
192 193 $submissionId = Arr::get($attributes, 'entry_id');
193 194
195 + // When an entry is targeted, the authoritative form is the one stored on
196 + // that submission — never a separately supplied request form_id, which
197 + // could point counts/labels/fields/metadata reads at another form the
198 + // caller was not authorized for (authorization resolved from the entry).
199 + if ($submissionId) {
200 + $submission = $this->model->find($submissionId);
201 + if ($submission) {
202 + $formId = (int) $submission->form_id;
203 + }
204 + }
205 +
194 206 if (Arr::get($attributes, 'counts')) {
195 207 $resources['counts'] = $this->model->countByGroup($formId);
196 208 }
197 209
@@ -278,8 +290,20 @@
278 290 $submissionId = intval(Arr::get($attributes, 'entry_id'));
279 291
280 292 $status = sanitize_text_field(Arr::get($attributes, 'status'));
281 293
294 + $submission = $this->model->find($submissionId);
295 +
296 + if (!$submission) {
297 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
298 + throw new Exception(__('Submission not found', 'fluentform'));
299 + }
300 +
301 + if (!isset(Helper::getMutableEntryStatuses($submission->form_id, $submissionId)[$status])) {
302 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
303 + throw new Exception(__('Invalid entry status', 'fluentform'));
304 + }
305 +
282 306 $this->model->amend($submissionId, ['status' => $status]);
283 307
284 308 do_action('fluentform/after_submission_status_update', $submissionId, $status);
285 309
@@ -336,15 +360,26 @@
336 360 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
337 361 throw new Exception(__('Please select entries first', 'fluentform'));
338 362 }
339 363
364 + // Re-verify against the form actually mutated; the policy scopes on a request entry_id.
365 + if (!FormManagerService::hasFormPermission($formId)) {
366 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
367 + throw new Exception(__('You do not have permission to modify this form\'s entries.', 'fluentform'));
368 + }
369 +
340 370 $query = $this->model->where('form_id', $formId)->whereIn('id', $submissionIds);
341 371
342 - $statuses = Helper::getEntryStatuses($formId);
372 + $statuses = Helper::getMutableEntryStatuses($formId);
343 373
344 374 $message = '';
345 375
346 376 if (isset($statuses[$actionType])) {
377 + // Hook only ids this form owns; array_flip keeps the caller's own id values and order.
378 + $ownedIds = array_flip(
379 + $this->model->where('form_id', $formId)->whereIn('id', $submissionIds)->pluck('id')->all()
380 + );
381 +
347 382 $query->update([
348 383 'status' => $actionType,
349 384 'updated_at' => current_time('mysql'),
350 385 ]);
@@ -349,9 +384,11 @@
349 384 'updated_at' => current_time('mysql'),
350 385 ]);
351 386
352 387 foreach ($submissionIds as $submissionId) {
353 - do_action('fluentform/after_submission_status_update', $submissionId, $actionType);
388 + if (isset($ownedIds[$submissionId])) {
389 + do_action('fluentform/after_submission_status_update', $submissionId, $actionType);
390 + }
354 391 }
355 392
356 393 $message = 'Selected entries successfully marked as ' . $statuses[$actionType];
357 394 } elseif ('other.delete_permanently' == $actionType) {
@@ -435,10 +472,18 @@
435 472 if ($shouldDelete) {
436 473 $deletables = $this->getAttachments($submissionIds, $formId);
437 474
438 475 foreach ($deletables as $file) {
439 - $file = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/' . basename($file);
476 + $fileName = basename($file);
440 477
478 + // Plugin uploads are ff-prefixed, so guard files and dot-files can only be crafted.
479 + if ('' === $fileName || '.' === $fileName[0]
480 + || in_array(strtolower($fileName), ['index.php', 'web.config'], true)) {
481 + continue;
482 + }
483 +
484 + $file = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/' . $fileName;
485 +
441 486 if (is_readable($file) && !is_dir($file)) {
442 487 wp_delete_file($file);
443 488 }
444 489 }
@@ -446,10 +491,12 @@
446 491 if (defined('FLUENTFORMPRO')) {
447 492 $tempDir = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/temp/';
448 493 $files = glob($tempDir . '*');
449 494 if(!empty($files)){
495 + // Shared across forms/visitors: only aged files, never an in-flight upload.
496 + $cutoff = time() - apply_filters('fluentform/temp_file_delete_time', 172800);
450 497 foreach ($files as $file) {
451 - if (basename($file) !== 'index.php') {
498 + if (basename($file) !== 'index.php' && is_file($file) && filemtime($file) < $cutoff) {
452 499 wp_delete_file($file);
453 500 }
454 501 }
455 502 }
@@ -553,9 +600,14 @@
553 600 }
554 601
555 602 public function storeNote($submissionId, $attributes = [])
556 603 {
557 - $formId = intval(Arr::get($attributes, 'form_id'));
604 + // SECURITY (FINDING-20): derive form_id from the route-authorized submission, not the
605 + // request body. Authorization is computed from the entry_id, but the note row's form_id
606 + // came straight from the body — letting a manager scoped to one form write a note row into
607 + // another form's meta namespace (cross-form integrity pollution).
608 + $submission = Submission::find($submissionId);
609 + $formId = $submission ? intval($submission->form_id) : intval(Arr::get($attributes, 'form_id'));
558 610
559 611 $content = sanitize_textarea_field($attributes['note']['content']);
560 612 $status = sanitize_text_field($attributes['note']['status']);
561 613 $user = get_user_by('ID', get_current_user_id());
@@ -653,16 +705,21 @@
653 705 );
654 706
655 707 do_action('fluentform/submission_user_changed', $submission, $user);
656 708
709 + // Email is roster PII; gate it on list_users (permalink self-gates). FF-SEC-45.
710 + $responseUser = [
711 + 'name' => $user->display_name,
712 + 'ID' => $user->ID,
713 + 'permalink' => get_edit_user_link($user->ID),
714 + ];
715 + if (current_user_can('list_users')) {
716 + $responseUser['email'] = $user->user_email;
717 + }
718 +
657 719 return ([
658 720 'message' => __('Selected user has been successfully assigned to this submission', 'fluentform'),
659 - 'user' => [
660 - 'name' => $user->display_name,
661 - 'email' => $user->user_email,
662 - 'ID' => $user->ID,
663 - 'permalink' => get_edit_user_link($user->ID),
664 - ],
721 + 'user' => $responseUser,
665 722 'user_id' => $userId,
666 723 ]);
667 724 }
668 725