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 +77 -13 6.2.3 → 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,15 +384,24 @@
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) {
358 - $this->deleteEntries($submissionIds, $formId);
395 + $ownedIds = $this->model->where('form_id', $formId)
396 + ->whereIn('id', $submissionIds)
397 + ->pluck('id')
398 + ->all();
359 399
400 + if ($ownedIds) {
401 + $this->deleteEntries($ownedIds, $formId);
402 + }
403 +
360 404 $message = __('Selected entries successfully deleted', 'fluentform');
361 405 } elseif ('other.make_favorite' == $actionType) {
362 406 $query->update([
363 407 'is_favourite' => 1,
@@ -391,9 +435,9 @@
391 435 }
392 436
393 437 $this->deleteFiles($submissionIds, $formId);
394 438
395 - Submission::remove($submissionIds);
439 + Submission::remove($submissionIds, $formId);
396 440
397 441 do_action('fluentform/after_deleting_submissions', $submissionIds, $formId);
398 442
399 443 foreach ($submissionIds as $submissionId) {
@@ -428,10 +472,18 @@
428 472 if ($shouldDelete) {
429 473 $deletables = $this->getAttachments($submissionIds, $formId);
430 474
431 475 foreach ($deletables as $file) {
432 - $file = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/' . basename($file);
476 + $fileName = basename($file);
433 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 +
434 486 if (is_readable($file) && !is_dir($file)) {
435 487 wp_delete_file($file);
436 488 }
437 489 }
@@ -439,10 +491,12 @@
439 491 if (defined('FLUENTFORMPRO')) {
440 492 $tempDir = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/temp/';
441 493 $files = glob($tempDir . '*');
442 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);
443 497 foreach ($files as $file) {
444 - if (basename($file) !== 'index.php') {
498 + if (basename($file) !== 'index.php' && is_file($file) && filemtime($file) < $cutoff) {
445 499 wp_delete_file($file);
446 500 }
447 501 }
448 502 }
@@ -546,9 +600,14 @@
546 600 }
547 601
548 602 public function storeNote($submissionId, $attributes = [])
549 603 {
550 - $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'));
551 610
552 611 $content = sanitize_textarea_field($attributes['note']['content']);
553 612 $status = sanitize_text_field($attributes['note']['status']);
554 613 $user = get_user_by('ID', get_current_user_id());
@@ -646,16 +705,21 @@
646 705 );
647 706
648 707 do_action('fluentform/submission_user_changed', $submission, $user);
649 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 +
650 719 return ([
651 720 'message' => __('Selected user has been successfully assigned to this submission', 'fluentform'),
652 - 'user' => [
653 - 'name' => $user->display_name,
654 - 'email' => $user->user_email,
655 - 'ID' => $user->ID,
656 - 'permalink' => get_edit_user_link($user->ID),
657 - ],
721 + 'user' => $responseUser,
658 722 'user_id' => $userId,
659 723 ]);
660 724 }
661 725