Skip to content

fix: harden GetLogs Livewire component properties - #9229

Merged
andrasbacsai merged 4 commits into
nextfrom
fix/harden-getlogs-livewire-properties
Mar 29, 2026
Merged

andrasbacsai merged 4 commits into
nextfrom
fix/harden-getlogs-livewire-properties

Conversation

@andrasbacsai

Copy link
Copy Markdown
Member

Summary

  • Add #[Locked] attributes to security-sensitive Livewire properties (resource, servicesubtype, server, container) in the GetLogs component to prevent unintended client-side modification
  • Add container name validation using ValidationPatterns::isValidContainerName() in getLogs() and downloadAllLogs() for input consistency
  • Add server ownership authorization check via Server::ownedByCurrentTeam() to both methods, aligning with the pattern used in ExecuteContainerCommand

Test plan

  • php artisan test --compact tests/Feature/GetLogsCommandInjectionTest.php — 14 tests pass
  • php artisan test --compact tests/Feature/DatabaseImportCommandInjectionTest.php — existing tests pass (no regression)
  • php artisan test --compact tests/Feature/CommandInjectionSecurityTest.php — broader security suite passes
  • Verify database/application/service log pages render correctly
  • Verify log streaming, timestamp toggling, and download all logs still work

🤖 Generated with Claude Code

andrasbacsai and others added 3 commits March 28, 2026 12:28
…ut validation

Add #[Locked] attributes to security-sensitive properties (resource, servicesubtype,
server, container) to prevent client-side modification via Livewire wire protocol.
Add container name validation using ValidationPatterns::isValidContainerName() and
server ownership authorization via Server::ownedByCurrentTeam() in both getLogs()
and downloadAllLogs() methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace $guarded = [] with explicit $fillable whitelists across all
models. Update controllers to use request->only($allowedFields) when
assigning request data. Switch Livewire components to forceFill() for
explicit mass assignment. Add integration tests for mass assignment
protection.
@andrasbacsai

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request implements mass-assignment protection across the application by replacing unguarded Eloquent models (protected $guarded = []) with explicit protected $fillable whitelists. Controllers that create or update resources now use $request->only($allowedFields) instead of $request->all() before passing data to models. Livewire components switch from fill() to forceFill() to respect the new fillable attributes during cloning operations. Security improvements include authorization checks and container-name validation in the GetLogs component via #[Locked] attributes. Additional refinactoring removes fully-qualified class name references and standardizes exception handling across multiple files. Two new test suites validate the mass-assignment rules and container-injection security controls.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/harden-getlogs-livewire-properties

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
app/Http/Controllers/Api/ApplicationsController.php (1)

1946-2003: ⚠️ Potential issue | 🟠 Major

Two dockercompose flags just became decorative switches.

This branch still documents use_build_server and connect_to_docker_network, but the dockercompose-specific $allowedFields omits both. With the new only($allowedFields) plus the extra-fields check, those requests now 422 before Service::fill() runs. Add both keys to the local allowlist or map them explicitly before filling the Service.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/Api/ApplicationsController.php` around lines 1946 -
2003, The request allowlist ($allowedFields) used before Service::fill() is
missing the docker-compose flags use_build_server and connect_to_docker_network,
causing valid requests to 422; update the $allowedFields array to include
'use_build_server' and 'connect_to_docker_network' or, alternatively, set those
two keys on the Request (or on $service) explicitly after the extra-fields check
but before calling removeUnnecessaryFieldsFromRequest() and
$service->fill($request->only($allowedFields)), ensuring the flags are present
when Service::fill() runs.
app/Http/Controllers/Api/SecurityController.php (1)

298-334: ⚠️ Potential issue | 🔴 Critical

This PATCH action terminates itself before the update.

$foundKey is resolved with $request->uuid, but uuid is neither part of the route nor allowed by the extra-fields guard, and the request schema above disallows it too. Any client that sends uuid gets a 422 before the lookup, so this endpoint cannot update anything under its advertised contract. Accept uuid as a validated identifier (ideally a route param) while keeping it out of the mass-assignment payload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/Api/SecurityController.php` around lines 298 - 334, The
PATCH currently fails because the lookup uses $request->uuid but uuid is neither
accepted by customApiValidator nor included in $allowedFields, so sending uuid
triggers the extra-fields validation and prevents the update; fix by accepting
uuid as a validated identifier but not as a mass-assignable field: retrieve uuid
from the route (e.g., $uuid = $request->route('uuid')) or add 'uuid' to your
validator rules as a required|string but do NOT include 'uuid' in
$allowedFields, adjust the extraFields check to ignore route-sourced uuid (or
remove it from array_diff) and use PrivateKey::where('team_id',
$teamId)->where('uuid', $uuid)->first() so
$foundKey->update($request->only($allowedFields)) continues to exclude uuid from
mass assignment.
bootstrap/helpers/applications.php (1)

209-221: ⚠️ Potential issue | 🔴 Critical

Persist the destination morph type during application clone.

This payload updates destination_id but not destination_type, and replicate() preserves the old morph class. Cloning between different destination backends will save a broken relation that points at the new id in the old table. The clone lives; the morph pointer time-travels.

🔧 Suggested fix
     ])->forceFill(array_merge([
         'uuid' => $uuid,
         'name' => $name,
         'fqdn' => $url,
         'status' => 'exited',
         'destination_id' => $destination->id,
+        'destination_type' => $destination->getMorphClass(),
     ], $overrides));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bootstrap/helpers/applications.php` around lines 209 - 221, The clone
currently updates destination_id but leaves the old morph class intact; update
the forceFill payload for $newApplication (the result of
$source->replicate([...])->forceFill(...)) to also set 'destination_type' to the
correct morph class for the new destination (e.g. $destination->getMorphClass()
or get_class($destination)) so the cloned application points to the correct
polymorphic table.
app/Livewire/Project/Shared/ResourceOperations.php (1)

93-103: ⚠️ Potential issue | 🔴 Critical

Set destination_type on cloned databases.

replicate() keeps the source morph type. If this action clones a database from StandaloneDocker to SwarmDocker or back, only swapping destination_id leaves the new row resolving against the wrong destination table. Tiny Skynet bug. Write both keys from $new_destination here.

🔧 Suggested fix
             ])->forceFill([
                 'uuid' => $uuid,
                 'name' => $this->resource->name.'-clone-'.$uuid,
                 'status' => 'exited',
                 'started_at' => null,
                 'destination_id' => $new_destination->id,
+                'destination_type' => $new_destination->getMorphClass(),
             ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Livewire/Project/Shared/ResourceOperations.php` around lines 93 - 103,
The clone currently only updates destination_id leaving destination_type from
the source; update the forceFill call on the replicated resource so it sets both
destination_id and destination_type using the destination object's morph class
(e.g. $new_destination->getMorphClass()) so the cloned $new_resource resolves to
the correct destination table; locate the replicate(...)->forceFill(...) chain
in ResourceOperations.php and add destination_type =>
$new_destination->getMorphClass() alongside destination_id.
app/Livewire/Project/CloneMe.php (1)

91-149: ⚠️ Potential issue | 🔴 Critical

Re-resolve and authorize the selected destination server-side.

selectedDestination is a public Livewire property, so the client can tamper with it. This action only checks that it exists; applications try to look up a destination from component state, but databases and services copy the raw id straight into destination_id, and those payloads never persist destination_type. That opens an authorization hole and can save broken morphs when the backend type changes. Query the destination from currentTeam() inside the action, add the action-level authorization check, and use its id + getMorphClass() for every clone.

Based on learnings: Validate and authorize in Livewire component actions; they behave like HTTP requests.

Also applies to: 255-265

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Livewire/Project/CloneMe.php` around lines 91 - 149, The clone action is
trusting the public Livewire property selectedDestination; re-resolve and
authorize the destination on the server and use its id + morph class for all
clones: inside clone(string $type) query the destination via
currentTeam()->servers()->with('destinations')->whereHas('destinations',
fn($q)=>$q->where('id',$this->selectedDestination))->firstOrFail() and then find
the destination model (e.g. $destination =
$server->destinations()->where('id',$this->selectedDestination)->firstOrFail()),
run an authorization check (authorize or
Gate::forUser(currentUser())->check(...) as your app uses) and then pass that
$destination object into clone_application($application, $destination, ...)
instead of resolving from $this->servers; when creating databases and services
replace direct destination_id = $this->selectedDestination with destination_id =
$destination->id and set destination_type = $destination->getMorphClass() so
both id and type are persisted consistently and safely (also apply same change
in the other clone block referenced for lines ~255-265).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Models/Application.php`:
- Around line 1769-1771: The RuntimeException throw exposes raw git errors (e.g.
"{$gitRemoteStatus['error']}") which can contain credentials; replace the
user-facing error text with a redacted or generic message (e.g. "Failed to read
Git source; check repository access") and ensure the full
$gitRemoteStatus['error'] is written only to internal/server logs (not returned
to the UI). Apply this change where the code checks
$gitRemoteStatus['is_accessible'] and throws RuntimeException (the shown throw
and the other occurrence around 1813-1829), and implement a small sanitizer if
you prefer to keep partial context (strip user:pass@ from URLs) before including
any shortened detail in the public message.

In `@app/Models/StandaloneClickhouse.php`:
- Around line 19-20: The Eloquent model StandaloneClickhouse has a typo in its
$casts array: it casts 'clickhouse_password' instead of the actual column name
'clickhouse_admin_password', so the admin password is not being encrypted;
update the $casts entry to use the exact column key 'clickhouse_admin_password'
(matching the $fillable entry) and ensure the cast uses the 'encrypted' or
equivalent cast type used elsewhere so the password is stored encrypted.

In `@app/Models/StandaloneRedis.php`:
- Around line 16-20: The model currently exposes 'redis_password' in the
$fillable array even though create_standalone_redis() stores the password in the
REDIS_PASSWORD env var and redisPassword() reads it from
runtime_environment_variables(); remove 'redis_password' from the
StandaloneRedis::$fillable to prevent misleading mass-assignment, and if you
need runtime password updates implement them via the environment-variable update
path used by runtime_environment_variables() (e.g., update logic invoked by
create_standalone_redis() or a dedicated env-var updater) rather than relying on
Model::update($request->only(['redis_password'])).

In `@app/Models/Team.php`:
- Around line 43-50: Update the Team model's $fillable array to remove migrated
attributes and add the missing personal_team: remove
'use_instance_email_settings' and 'resend_api_key' from Team::$fillable and
ensure 'personal_team' is present alongside 'name', 'description',
'show_boarding', and 'custom_server_limit'; leave notification fields on
EmailNotificationSettings model only. Then in Api/TeamController
(removeSensitiveData method) delete the code that attempts to hide
'smtp_username', 'smtp_password', 'telegram_token', and 'resend_api_key' from
Team instances since those columns no longer exist on the teams table and are
handled by their respective notification settings models.

In `@app/Models/User.php`:
- Around line 46-55: The User model has removed 'id' from $fillable which causes
calls like User::create(['id' => 0, ...]) in
app/Actions/Fortify/CreateNewUser.php and database/seeders/RootUserSeeder.php to
silently drop the id (breaking the root-user bootstrap and User::boot()
special-case); update those trusted writers to set the id explicitly using
forceFill(['id' => 0])->save() or assign $user->id = 0 before saving (or
otherwise use manual assignment) instead of relying on mass-assignment, ensuring
the root seeder and CreateNewUser action create the root user with id 0 so
User::boot() can create the root team.
- Around line 46-55: Remove 'pending_email', 'email_change_code', and
'email_change_code_expires_at' from the User::$fillable array so they are not
mass-assignable, and update the email-change flow to set those fields using
$this->forceFill([...])->save() inside the requestEmailChange() method (keeping
the server-generated $code and $expiresAt). Also add or update
MassAssignmentProtectionTest to assert these fields cannot be mass-assigned to
prevent regressions.

In `@tests/Feature/GetLogsCommandInjectionTest.php`:
- Around line 38-55: Replace the reflection-based tests with Livewire action
tests that exercise the actual Livewire component invoking GetLogs::getLogs and
GetLogs::downloadAllLogs: call the Livewire action with invalid container names
and with a server not owned by the current team, assert the response
aborts/returns authorization/validation errors and ensure no docker/command
execution path is reached by spying/mocking the command execution helper (or
asserting the command runner was not called). Specifically target the Livewire
component method that routes to GetLogs actions, provide inputs that would fail
ValidationPatterns::isValidContainerName and a Server where
Server::ownedByCurrentTeam() is false, and assert validation/authorization
happens before any command assembly or runner invocation. Ensure you replace the
ReflectionMethod checks for strings with these interaction assertions.

In `@tests/Feature/MassAssignmentProtectionTest.php`:
- Around line 109-114: The Team model's personal_team attribute is not fillable
so calls like Team::create(['personal_team' => true, ...]) from User::boot() and
User::recreate_personal_team() silently drop the flag; fix by either (A)
updating User::boot() and User::recreate_personal_team() to set the flag after
creation (e.g., create the Team, then $team->personal_team = true;
$team->save()) or (B) use $team = Team::forceCreate(...) or $team = (new
Team)->forceFill([...])->save() when creating personal teams; do not change
tests—modify the Team creation code paths in the User methods referenced to
ensure personal_team is persisted.

---

Outside diff comments:
In `@app/Http/Controllers/Api/ApplicationsController.php`:
- Around line 1946-2003: The request allowlist ($allowedFields) used before
Service::fill() is missing the docker-compose flags use_build_server and
connect_to_docker_network, causing valid requests to 422; update the
$allowedFields array to include 'use_build_server' and
'connect_to_docker_network' or, alternatively, set those two keys on the Request
(or on $service) explicitly after the extra-fields check but before calling
removeUnnecessaryFieldsFromRequest() and
$service->fill($request->only($allowedFields)), ensuring the flags are present
when Service::fill() runs.

In `@app/Http/Controllers/Api/SecurityController.php`:
- Around line 298-334: The PATCH currently fails because the lookup uses
$request->uuid but uuid is neither accepted by customApiValidator nor included
in $allowedFields, so sending uuid triggers the extra-fields validation and
prevents the update; fix by accepting uuid as a validated identifier but not as
a mass-assignable field: retrieve uuid from the route (e.g., $uuid =
$request->route('uuid')) or add 'uuid' to your validator rules as a
required|string but do NOT include 'uuid' in $allowedFields, adjust the
extraFields check to ignore route-sourced uuid (or remove it from array_diff)
and use PrivateKey::where('team_id', $teamId)->where('uuid', $uuid)->first() so
$foundKey->update($request->only($allowedFields)) continues to exclude uuid from
mass assignment.

In `@app/Livewire/Project/CloneMe.php`:
- Around line 91-149: The clone action is trusting the public Livewire property
selectedDestination; re-resolve and authorize the destination on the server and
use its id + morph class for all clones: inside clone(string $type) query the
destination via
currentTeam()->servers()->with('destinations')->whereHas('destinations',
fn($q)=>$q->where('id',$this->selectedDestination))->firstOrFail() and then find
the destination model (e.g. $destination =
$server->destinations()->where('id',$this->selectedDestination)->firstOrFail()),
run an authorization check (authorize or
Gate::forUser(currentUser())->check(...) as your app uses) and then pass that
$destination object into clone_application($application, $destination, ...)
instead of resolving from $this->servers; when creating databases and services
replace direct destination_id = $this->selectedDestination with destination_id =
$destination->id and set destination_type = $destination->getMorphClass() so
both id and type are persisted consistently and safely (also apply same change
in the other clone block referenced for lines ~255-265).

In `@app/Livewire/Project/Shared/ResourceOperations.php`:
- Around line 93-103: The clone currently only updates destination_id leaving
destination_type from the source; update the forceFill call on the replicated
resource so it sets both destination_id and destination_type using the
destination object's morph class (e.g. $new_destination->getMorphClass()) so the
cloned $new_resource resolves to the correct destination table; locate the
replicate(...)->forceFill(...) chain in ResourceOperations.php and add
destination_type => $new_destination->getMorphClass() alongside destination_id.

In `@bootstrap/helpers/applications.php`:
- Around line 209-221: The clone currently updates destination_id but leaves the
old morph class intact; update the forceFill payload for $newApplication (the
result of $source->replicate([...])->forceFill(...)) to also set
'destination_type' to the correct morph class for the new destination (e.g.
$destination->getMorphClass() or get_class($destination)) so the cloned
application points to the correct polymorphic table.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b9e35ff8-9f66-4e94-9b95-d47b2432d545

📥 Commits

Reviewing files that changed from the base of the PR and between 98569e4 and 67a4fcc.

📒 Files selected for processing (22)
  • app/Http/Controllers/Api/ApplicationsController.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Http/Controllers/Api/SecurityController.php
  • app/Livewire/Project/CloneMe.php
  • app/Livewire/Project/Shared/GetLogs.php
  • app/Livewire/Project/Shared/ResourceOperations.php
  • app/Models/Application.php
  • app/Models/Server.php
  • app/Models/Service.php
  • app/Models/StandaloneClickhouse.php
  • app/Models/StandaloneDragonfly.php
  • app/Models/StandaloneKeydb.php
  • app/Models/StandaloneMariadb.php
  • app/Models/StandaloneMongodb.php
  • app/Models/StandaloneMysql.php
  • app/Models/StandalonePostgresql.php
  • app/Models/StandaloneRedis.php
  • app/Models/Team.php
  • app/Models/User.php
  • bootstrap/helpers/applications.php
  • tests/Feature/GetLogsCommandInjectionTest.php
  • tests/Feature/MassAssignmentProtectionTest.php
💤 Files with no reviewable changes (1)
  • app/Models/Server.php

Comment thread app/Models/Application.php
Comment thread app/Models/StandaloneClickhouse.php
Comment thread app/Models/StandaloneRedis.php
Comment thread app/Models/Team.php
Comment thread app/Models/User.php
Comment thread tests/Feature/GetLogsCommandInjectionTest.php Outdated
Comment thread tests/Feature/MassAssignmentProtectionTest.php
Restrict mass-assignable attributes across user/team/redis models and
switch privileged root/team creation paths to forceFill/forceCreate.

Encrypt legacy ClickHouse admin passwords via migration and cast the
correct ClickHouse password field as encrypted.

Tighten API and runtime exposure by removing sensitive team fields from
responses and sanitizing Git/compose error messages.

Expand security-focused feature coverage for command-injection and mass
assignment protections.
@andrasbacsai
andrasbacsai merged commit f267a28 into next Mar 29, 2026
4 checks passed
@andrasbacsai
andrasbacsai deleted the fix/harden-getlogs-livewire-properties branch March 29, 2026 19:29
@andrasbacsai andrasbacsai mentioned this pull request Apr 5, 2026
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Apr 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant