fix(sentinel): validate push containers payload

Reject malformed sentinel push payloads before updating heartbeat state,
dispatching jobs, or writing deduplication cache entries.
This commit is contained in:
Andras Bacsai 2026-05-26 14:07:41 +02:00
parent ed3780b2a7
commit 7677fac2f5
2 changed files with 33 additions and 0 deletions

View file

@ -8,6 +8,7 @@
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class SentinelController extends Controller
{
@ -77,6 +78,17 @@ public function push(Request $request)
return response()->json(['message' => 'Unauthorized'], 401);
}
$validator = Validator::make($request->all(), [
'containers' => ['required', 'array', 'min:1'],
]);
if ($validator->fails()) {
return response()->json(serializeApiResponse([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
]), 422);
}
$data = $request->all();
// Heartbeat MUST update on every push — drives isSentinelLive() and SSH-check skipping.

View file

@ -11,6 +11,8 @@
uses(RefreshDatabase::class);
beforeEach(function () {
config(['app.maintenance.store' => 'array']);
Queue::fake();
Cache::flush();
@ -69,6 +71,25 @@ function sentinelPayload(array $containers, ?float $diskPercentage = 42.0): arra
expect(Carbon::parse($this->server->fresh()->sentinel_updated_at)->diffInSeconds(now()))->toBeLessThan(5);
});
it('rejects malformed sentinel payloads before touching server state', function (array $payload) {
$this->server->update(['sentinel_updated_at' => now()->subHour()]);
$originalHeartbeat = $this->server->fresh()->sentinel_updated_at;
pushSentinel($this->token, $payload)
->assertUnprocessable()
->assertJsonPath('message', 'Validation failed.')
->assertJsonValidationErrors('containers');
Queue::assertNotPushed(PushServerUpdateJob::class);
expect($this->server->fresh()->sentinel_updated_at)->toBe($originalHeartbeat);
expect(Cache::has('sentinel:push-hash:'.$this->server->id))->toBeFalse();
expect(Cache::has('sentinel:push-force:'.$this->server->id))->toBeFalse();
})->with([
'missing containers' => [[]],
'non-array containers' => [['containers' => 'not-an-array']],
'empty containers' => [['containers' => []]],
]);
it('dispatches the job when container state changes', function () use ($running) {
pushSentinel($this->token, sentinelPayload($running()))->assertOk();