fix(env): prevent copying unauthorized and shown-once values

Lock compose variable lookup fields against client changes, check resource update permission before resolving references, and exclude shown-once variables from reference lookup.
This commit is contained in:
Andras Bacsai
2026-09-25 07:30:17 +02:00
parent 9b59acdb55
commit 19744f9c26
3 changed files with 60 additions and 2 deletions
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Attributes\Locked;
use Livewire\Component;
class ShowHardcoded extends Component
@@ -13,16 +14,20 @@ class ShowHardcoded extends Component
public string $key;
#[Locked]
public ?string $value = null;
public ?string $comment = null;
public ?string $serviceName = null;
#[Locked]
public bool $isPreview = false;
#[Locked]
public ?string $resourceableType = null;
#[Locked]
public ?int $resourceableId = null;
public function mount()
@@ -39,12 +44,19 @@ class ShowHardcoded extends Component
return null;
}
return EnvironmentVariable::make([
$environmentVariable = EnvironmentVariable::make([
'value' => $this->value,
'is_preview' => $this->isPreview,
'resourceable_type' => $this->resourceableType,
'resourceable_id' => $this->resourceableId,
])->resolveReferencedValue();
]);
$resource = $environmentVariable->resourceable;
if (! $resource || auth()->user()->cannot('update', $resource)) {
return null;
}
return $environmentVariable->resolveReferencedValue();
}
public function render()
+1
View File
@@ -346,6 +346,7 @@ class EnvironmentVariable extends BaseModel
return static::where('resourceable_type', $this->resourceable_type)
->where('resourceable_id', $this->resourceable_id)
->where('is_preview', (bool) $this->is_preview)
->where('is_shown_once', false)
->where('key', $referencedKey)
->first()?->value ?? $value;
}
@@ -7,10 +7,12 @@ use App\Models\Environment;
use App\Models\EnvironmentVariable;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Service;
use App\Models\SharedEnvironmentVariable;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
use Livewire\Livewire;
uses(RefreshDatabase::class);
@@ -135,6 +137,49 @@ test('compose-managed rows copy the referenced variable value', function () {
assertCopiedComposeValue('production', 'production');
});
test('copying a reference does not reveal a locked variable', function () {
createEnvironmentVariable(['key' => 'SERVICE_PASSWORD_MYSQL', 'value' => 'locked-password', 'is_shown_once' => true]);
assertCopiedValue(
createEnvironmentVariable(['key' => 'DB_PASSWORD', 'value' => '$SERVICE_PASSWORD_MYSQL']),
'$SERVICE_PASSWORD_MYSQL',
);
assertCopiedComposeValue('$SERVICE_PASSWORD_MYSQL', '$SERVICE_PASSWORD_MYSQL');
});
test('compose-managed rows do not accept a changed lookup from the client', function (string $property, mixed $value) {
Livewire::test(ShowHardcoded::class, [
'env' => ['key' => 'MYSQL_USER', 'value' => 'production'],
'resourceableType' => Application::class,
'resourceableId' => test()->application->id,
])->set($property, $value);
})->with([
'value' => ['value', '$DATABASE_PASSWORD'],
'preview scope' => ['isPreview', true],
'resource type' => ['resourceableType', Service::class],
'resource id' => ['resourceableId', 999],
])->throws(CannotUpdateLockedPropertyException::class);
test('compose-managed rows do not copy values from another team', function () {
$otherTeam = Team::factory()->create();
$otherProject = Project::factory()->create(['team_id' => $otherTeam->id]);
$otherEnvironment = Environment::factory()->create(['project_id' => $otherProject->id]);
$otherApplication = Application::factory()->create(['environment_id' => $otherEnvironment->id]);
createEnvironmentVariable([
'key' => 'DATABASE_PASSWORD',
'value' => 'other-team-secret',
'resourceable_id' => $otherApplication->id,
]);
Livewire::test(ShowHardcoded::class, [
'env' => ['key' => 'DB_PASSWORD', 'value' => '$DATABASE_PASSWORD'],
'resourceableType' => Application::class,
'resourceableId' => $otherApplication->id,
])
->call('copyValue')
->assertReturned(null);
});
test('compose-managed rows hide copying from members', function () {
$member = User::factory()->create();
$this->team->members()->attach($member, ['role' => 'member']);