Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b3bc1c0
FOUR-32465: [Octane] CRITICAL Data Leaks Between Requests "$uid2id"
rodriquelca Jul 30, 2026
214ac42
feat: refactor redirect handling by introducing RedirectToEventService
devmiguelangel Jul 31, 2026
53c44b2
Merge remote-tracking branch 'origin/feature/FOUR-30918' into feature…
rodriquelca Jul 31, 2026
7f405b4
test: add unit tests for BpmnAction and RedirectToEventService functi…
devmiguelangel Jul 31, 2026
05d250f
Merge remote-tracking branch 'origin/feature/FOUR-32353' into feature…
rodriquelca Jul 31, 2026
2ff685c
feat(FOUR-32473): [Octane] CRITICAL Data Leaks Between Requests "$red…
rodriquelca Jul 31, 2026
f0ba618
FOUR-32474 [Octane] CRITICAL Data Leaks Between Requests "$landlordVa…
gproly Jul 31, 2026
6775f5e
FOUR-32475 [Octane] CRITICAL Data Leaks Between Requests "AnonymousUs…
gproly Jul 31, 2026
a32cdc6
Merge remote-tracking branch 'origin/feature/FOUR-30918' into feature…
rodriquelca Jul 31, 2026
99c4147
Merge remote-tracking branch 'origin/feature/FOUR-30918' into feature…
rodriquelca Jul 31, 2026
3c628d3
Merge remote-tracking branch 'origin/feature/FOUR-30918' into FOUR-32466
devmiguelangel Jul 31, 2026
9d0d012
feat: refactor ResetRequestState to use RedirectToEventService for ha…
devmiguelangel Jul 31, 2026
51a4444
test: update ResetRequestStateTest to utilize app() for dependency re…
devmiguelangel Jul 31, 2026
0c58e63
test: enhance ResetRequestStateTest with query duration tracking and …
devmiguelangel Jul 31, 2026
ee03183
test: add query duration tracking and isolate metrics in ServerTiming…
devmiguelangel Jul 31, 2026
fde5410
FOUR-32498 [Octane] MEDIUM — Accumulating State "addons"
gproly Jul 31, 2026
27fdaec
Merge pull request #8951 from ProcessMaker/feature/FOUR-32465
pmPaulis Jul 31, 2026
e628337
Merge pull request #8956 from ProcessMaker/feature/FOUR-32473
pmPaulis Jul 31, 2026
560b457
Merge remote-tracking branch 'origin/feature/FOUR-32464' into FOUR-32466
devmiguelangel Jul 31, 2026
3392225
test: enhance HandleRedirectListenerTest to utilize RedirectToEventSe…
devmiguelangel Jul 31, 2026
6d4fc34
Merge pull request #8954 from ProcessMaker/FOUR-32466
pmPaulis Aug 3, 2026
b40ef22
Merge pull request #8955 from ProcessMaker/feature/FOUR-32474
pmPaulis Aug 3, 2026
64060eb
Merge pull request #8957 from ProcessMaker/feature/FOUR-32475
pmPaulis Aug 3, 2026
78e5b1b
Merge pull request #8961 from ProcessMaker/feature/FOUR-32498
pmPaulis Aug 3, 2026
5f0fa0f
Merge branches 'feature/FOUR-32464' and 'FOUR-32496' of github.com:Pr…
devmiguelangel Aug 3, 2026
743dde4
feat: update SettingObserver to use instance property for artisan cac…
devmiguelangel Aug 3, 2026
c6fee3d
test: add unit tests to validate callback scheduling and cache invali…
devmiguelangel Aug 3, 2026
96fbfd3
FOUR-32507 [Octane] MEDIUM — Accumulating State "ServerTimingMiddleware"
gproly Aug 3, 2026
1f00e5d
Merge pull request #8960 from ProcessMaker/FOUR-32496
pmPaulis Aug 3, 2026
562dcc4
Merge pull request #8965 from ProcessMaker/FOUR-32505
pmPaulis Aug 3, 2026
3752453
Merge branch 'feature/FOUR-32464' into feature/FOUR-32507
gproly Aug 3, 2026
bcfcbcf
Merge pull request #8966 from ProcessMaker/feature/FOUR-32507
pmPaulis Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions ProcessMaker/Http/Middleware/ServerTimingMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,10 @@

class ServerTimingMiddleware
{
// Minimum time in ms to include a package in the Server-Timing header
private static $minPackageTime;

public function __construct()
{
self::$minPackageTime = config('app.server_timing.min_package_time');
}

/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
Expand Down Expand Up @@ -56,12 +48,13 @@ public function handle(Request $request, Closure $next): Response
}

$packageTimes = ProcessMakerServiceProvider::getPackageBootTiming();
$minPackageTime = config('app.server_timing.min_package_time');

foreach ($packageTimes as $package => $timing) {
$time = ($timing['end'] - $timing['start']) * 1000;

// Only include packages that took more than MIN_PACKAGE_TIME ms
if ($time > self::$minPackageTime) {
if ($time > $minPackageTime) {
$serverTiming[] = "{$package};dur={$time}";
}
}
Expand Down
6 changes: 4 additions & 2 deletions ProcessMaker/Jobs/BpmnAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
use Illuminate\Support\Facades\Log;
use ProcessMaker\BpmnEngine;
use ProcessMaker\Exception\HttpABTestingException;
use ProcessMaker\Listeners\HandleRedirectListener;
use ProcessMaker\Models\Process as Definitions;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestLock;
use ProcessMaker\Services\RedirectToEventService;
use Throwable;

abstract class BpmnAction implements ShouldQueue
Expand Down Expand Up @@ -60,6 +60,7 @@ abstract class BpmnAction implements ShouldQueue
public function handle()
{
$response = null;
$redirectToEventService = app(RedirectToEventService::class);
try {
extract($this->loadContext());
$this->engine = $engine;
Expand All @@ -74,7 +75,7 @@ public function handle()
// (e.g. completed, assigned, process completed, etc)
// excluding system process (non_persistent_process)
if ($this->processId !== 'non_persistent_process') {
HandleRedirectListener::sendRedirectToEvent();
$redirectToEventService->sendRedirectToEvent();
}
} catch (HttpABTestingException $exception) {
Log::error($exception->getMessage());
Expand All @@ -87,6 +88,7 @@ public function handle()
$request->logError($exception, $element);
}
} finally {
$redirectToEventService->reset();
$this->unlock();
}

Expand Down
46 changes: 7 additions & 39 deletions ProcessMaker/Listeners/HandleRedirectListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,19 @@

namespace ProcessMaker\Listeners;

use ProcessMaker\Events\RedirectToEvent;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Services\RedirectToEventService;

class HandleRedirectListener
{
private static $processRequest = null;

protected static $redirectionMethod = '';

private static $redirectionParams = [];

protected function setRedirectTo(ProcessRequest $processRequest, string $method, ...$params): void
{
self::$processRequest = $processRequest;
self::$redirectionMethod = $method;
self::$redirectionParams = $params;
public function __construct(
private ?RedirectToEventService $redirectToEventService = null
) {
}

/**
* Reset the static state for Octane compatibility.
* This prevents data leaks between requests in long-running workers.
*/
public static function reset(): void
{
self::$processRequest = null;
self::$redirectionMethod = '';
self::$redirectionParams = [];
}

public static function sendRedirectToEvent()
protected function setRedirectTo(ProcessRequest $processRequest, string $method, ...$params): void
{
$method = self::$redirectionMethod;
$params = self::$redirectionParams;
$processRequest = self::$processRequest;

// Only get active tokens if there is a valid process request
if ($processRequest !== null) {
$params['activeTokens'] = ProcessRequest::getActiveTokens($processRequest);
$event = new RedirectToEvent($processRequest, $method, $params);
event($event);

// Clean params to prevent sending the same redirect multiple times
self::$redirectionParams = [];
self::$redirectionMethod = '';
self::$processRequest = null;
}
$this->redirectToEventService ??= app(RedirectToEventService::class);
$this->redirectToEventService->setRedirectTo($processRequest, $method, ...$params);
}
}
45 changes: 45 additions & 0 deletions ProcessMaker/Managers/ControllerAddonsRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace ProcessMaker\Managers;

class ControllerAddonsRegistry
{
private $addons = [];

/**
* Register a controller addon.
*/
public function register(string $scope, array $config): void
{
$config['scope'] = $scope;
$this->addons[] = $config;
}

/**
* Get configured addons for a controller.
*/
public function getAddons(string $scope, string $method, array $data): array
{
$addons = [];

foreach ($this->addons as $addon) {
if ($addon['method'] !== $method || $addon['scope'] !== $scope) {
continue;
}

if (isset($addon['data']) && is_callable($addon['data'])) {
$data = call_user_func($addon['data'], $data);
}

$addon['content'] = isset($addon['view']) && !isset($addon['content'])
? view($addon['view'], $data)->render()
: (isset($addon['content']) ? $addon['content'] : '');
$addon['script'] = isset($addon['script']) && is_string($addon['script'])
? view($addon['script'], $data)->render()
: '';
$addons[] = $addon;
}

return $addons;
}
}
6 changes: 6 additions & 0 deletions ProcessMaker/Models/AnonymousUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ class AnonymousUser extends User

protected $table = 'users';

public static function resolve(): self
{
return static::where('username', '=', static::ANONYMOUS_USERNAME)
->firstOrFail();
}

public $isAnonymous = true;

public function receivesBroadcastNotificationsOn($notification)
Expand Down
10 changes: 5 additions & 5 deletions ProcessMaker/Multitenancy/SwitchTenant.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Support\Arr;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Context;
use Monolog\Handler\RotatingFileHandler;
use ProcessMaker\Application;
use ProcessMaker\Multitenancy\Broadcasting\TenantAwareBroadcastManager;
Expand All @@ -17,7 +18,7 @@ class SwitchTenant implements SwitchTenantTask
{
use UsesMultitenancyConfig;

public static $landlordValues = null;
private const LANDLORD_VALUES_CONTEXT_KEY = 'multitenancy.landlord_values';

/**
* Make the given tenant current.
Expand All @@ -31,9 +32,8 @@ public function makeCurrent(IsTenant $tenant): void

\Log::debug('SwitchTenant: ' . $tenant->id, ['domain' => request()->getHost()]);

// Save the landlord values for later use
if (!self::$landlordValues) {
self::$landlordValues = $app->make('config')->all();
if (!Context::has(self::LANDLORD_VALUES_CONTEXT_KEY)) {
Context::add(self::LANDLORD_VALUES_CONTEXT_KEY, $app->make('config')->all());
}

// Set the tenant's domain in the request headers. Used for things like the global url() helper.
Expand Down Expand Up @@ -70,7 +70,7 @@ public function forgetCurrent(): void

private function landlordConfig($key)
{
return Arr::get(self::$landlordValues, $key);
return Arr::get(Context::get(self::LANDLORD_VALUES_CONTEXT_KEY), $key);
}

private function setConfig($key, $value)
Expand Down
10 changes: 5 additions & 5 deletions ProcessMaker/Nayra/Repositories/EntityRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

abstract class EntityRepository
{
private static $uid2id = ['requests' =>[], 'tokens' =>[]];
private $uid2id = ['requests' =>[], 'tokens' =>[]];

abstract public function create(array $transaction): ? Model;

Expand Down Expand Up @@ -41,16 +41,16 @@ public function resolveId(string $uid): int
}

// Get record if is not stored previously
if (!isset(self::$uid2id[$type][$uid])) {
if (!isset($this->uid2id[$type][$uid])) {
$record = $instance->select('id')->where('uuid', $uid)->first();
if ($record) {
self::$uid2id[$type][$uid] = $record->getKey();
$this->uid2id[$type][$uid] = $record->getKey();
} else {
throw new Exception("The uid {$uid} does not exist in the database");
}
}

return self::$uid2id[$type][$uid] ?? 0;
return $this->uid2id[$type][$uid] ?? 0;
}

/**
Expand All @@ -71,6 +71,6 @@ public function storeUid(string $uid, int $id): void
break;
}

self::$uid2id[$type][$uid] = $id;
$this->uid2id[$type][$uid] = $id;
}
}
6 changes: 3 additions & 3 deletions ProcessMaker/Observers/SettingObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class SettingObserver
{
private static $added_refresh_artisan_caches = false;
private bool $addedRefreshArtisanCaches = false;

/**
* Handle the setting "created" event.
Expand Down Expand Up @@ -95,7 +95,7 @@ private function invalidateSettingCache(Setting $setting)

// Check to see if we already added the refresh to the app's terminating queue.
// This is important for install commands when multiple settings are being created/updated.
if (self::$added_refresh_artisan_caches) {
if ($this->addedRefreshArtisanCaches) {
return;
}

Expand All @@ -106,6 +106,6 @@ private function invalidateSettingCache(Setting $setting)
RefreshArtisanCaches::dispatchSync();
});

self::$added_refresh_artisan_caches = true;
$this->addedRefreshArtisanCaches = true;
}
}
9 changes: 7 additions & 2 deletions ProcessMaker/Octane/ResetRequestState.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

namespace ProcessMaker\Octane;

use ProcessMaker\Listeners\HandleRedirectListener;
use ProcessMaker\Providers\ProcessMakerServiceProvider;
use ProcessMaker\Services\RedirectToEventService;

final class ResetRequestState
{
public function __construct(
private readonly RedirectToEventService $redirectToEventService
) {
}

public function handle(): void
{
ProcessMakerServiceProvider::beginRequestTiming();
HandleRedirectListener::reset();
$this->redirectToEventService->reset();
}
}
17 changes: 11 additions & 6 deletions ProcessMaker/Providers/ProcessMakerServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
use ProcessMaker\Providers\PermissionServiceProvider;
use ProcessMaker\Repositories\SettingsConfigRepository;
use ProcessMaker\Services\ConditionalRedirectService;
use ProcessMaker\Services\RedirectToEventService;
use RuntimeException;
use Spatie\Multitenancy\Events\MadeTenantCurrentEvent;
use Spatie\Multitenancy\Events\TenantNotFoundForRequestEvent;
Expand All @@ -78,9 +79,6 @@ class ProcessMakerServiceProvider extends ServiceProvider
// Track the query time for each request
private static $queryTime = 0;

// Track the landlord values for multitenancy
private static $landlordValues = null;

public function boot(): void
{
// Track the start time for service providers boot
Expand Down Expand Up @@ -136,6 +134,10 @@ public function register(): void
return new Managers\LoginManager();
});

$this->app->singleton(Managers\ControllerAddonsRegistry::class, function () {
return new Managers\ControllerAddonsRegistry();
});

/*
* Maps our Index Manager as a singleton. The Index Manager is used
* to manage customizations to the search indexer.
Expand Down Expand Up @@ -194,11 +196,12 @@ public function register(): void
return new Managers\GlobalScriptsManager();
});

$this->app->singleton(Models\AnonymousUser::class, function ($app) {
return Models\AnonymousUser::where('username', '=', Models\AnonymousUser::ANONYMOUS_USERNAME)
->firstOrFail();
$this->app->scoped(Models\AnonymousUser::class, function ($app) {
return Models\AnonymousUser::resolve();
});

$this->app->scoped(Observers\SettingObserver::class);

$this->app->singleton(PolicyExtension::class, function ($app) {
return new PolicyExtension();
});
Expand Down Expand Up @@ -249,6 +252,8 @@ public function register(): void

$this->app->instance('tenant-resolved', false);

$this->app->scoped(RedirectToEventService::class);

/**
* Conditional Redirect Service
* This service is used to evaluate the conditional redirect property of a process request token.
Expand Down
Loading
Loading