mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 03:06:23 +00:00
Merge remote-tracking branch 'origin/develop'
This commit is contained in:
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\AdminPersonalAccessTokens;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the Livewire snapshot-replay authorization bypass
|
||||
* reported by PizzaStev3 (2026-07-31). AdminPersonalAccessTokens' render()
|
||||
* enumerates every user's personal access tokens (name, expiration,
|
||||
* associated client, owning user). Route-level middleware on /admin/oauth
|
||||
* (superuser) protects the initial page render but not snapshot replays to
|
||||
* /livewire/update. boot() gate now catches both surfaces.
|
||||
*/
|
||||
class AdminPersonalAccessTokensAuthorizationTest extends TestCase
|
||||
{
|
||||
public function test_superuser_can_mount()
|
||||
{
|
||||
$this->actingAs(User::factory()->superuser()->create());
|
||||
|
||||
Livewire::test(AdminPersonalAccessTokens::class)
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_non_superuser_cannot_mount_or_replay()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(AdminPersonalAccessTokens::class)
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\CustomFieldSetDefaultValuesForModel;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the Livewire snapshot-replay authorization bypass
|
||||
* reported by PizzaStev3 (2026-07-31). The component renders custom-field
|
||||
* default values for an AssetModel identified by model_id. Route-level
|
||||
* middleware on the model create/edit pages requires models.edit, but
|
||||
* snapshot replay to /livewire/update bypassed that gate. boot() gate
|
||||
* now requires AssetModel update permission on both mount and any replayed
|
||||
* action.
|
||||
*/
|
||||
class CustomFieldSetDefaultValuesForModelAuthorizationTest extends TestCase
|
||||
{
|
||||
public function test_user_with_models_edit_permission_can_mount()
|
||||
{
|
||||
$model = AssetModel::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->editAssetModels()->create());
|
||||
|
||||
Livewire::test(CustomFieldSetDefaultValuesForModel::class, ['model_id' => $model->id])
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_user_without_models_edit_permission_cannot_mount_or_replay()
|
||||
{
|
||||
$model = AssetModel::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(CustomFieldSetDefaultValuesForModel::class, ['model_id' => $model->id])
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
@ -355,6 +355,29 @@ class ImporterTest extends TestCase
|
||||
->assertSet('activeFileRowCount', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression for Rollbar: selectFile() foreach()-on-null when the Import
|
||||
* row was persisted with header_row = null (legacy imports, or a background
|
||||
* job that never wrote the column). Previously exploded with
|
||||
* "foreach() argument must be of type array|object, null given" at the
|
||||
* headerRow loop. The guard now short-circuits with a translated error.
|
||||
*/
|
||||
public function test_selecting_a_file_with_null_header_row_shows_error_and_does_not_crash(): void
|
||||
{
|
||||
$user = User::factory()->canImport()->create();
|
||||
$import = Import::factory()->create([
|
||||
'created_by' => $user->id,
|
||||
'header_row' => null,
|
||||
]);
|
||||
$this->writeFakeImportFile($import, "asset tag\nAH-1\n");
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(Importer::class)
|
||||
->call('selectFile', $import->id)
|
||||
->assertSet('message_type', 'danger')
|
||||
->assertSet('message', trans('admin/hardware/message.import.header_row_missing'));
|
||||
}
|
||||
|
||||
public function test_next_step_from_type_selection_advances_when_type_is_set(): void
|
||||
{
|
||||
Storage::fake();
|
||||
|
||||
35
tests/Feature/Livewire/LdapSettingsAuthorizationTest.php
Normal file
35
tests/Feature/Livewire/LdapSettingsAuthorizationTest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\LdapSettings;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the Livewire snapshot-replay authorization bypass
|
||||
* reported by PizzaStev3 (2026-07-31). LdapSettings had an abort_unless
|
||||
* check in mount() which only fires on the initial page render. Snapshot
|
||||
* replay to /livewire/update went through hydrate() -> action methods
|
||||
* without re-entering mount(). Now duplicated into boot() which fires on
|
||||
* every Livewire request.
|
||||
*/
|
||||
class LdapSettingsAuthorizationTest extends TestCase
|
||||
{
|
||||
public function test_superadmin_can_mount()
|
||||
{
|
||||
$this->actingAs(User::factory()->firstAdmin()->create());
|
||||
|
||||
Livewire::test(LdapSettings::class)
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_non_superadmin_cannot_mount_or_replay()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(LdapSettings::class)
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\LocationScopeCheck;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the Livewire snapshot-replay authorization bypass
|
||||
* reported by PizzaStev3 (2026-07-31). LocationScopeCheck::check_locations()
|
||||
* surfaces cross-tenant FMCS-mismatch data. Route-level middleware on
|
||||
* /admin/settings (superuser) protects the initial page render but not
|
||||
* snapshot replays to /livewire/update. boot() gate now catches both.
|
||||
*/
|
||||
class LocationScopeCheckAuthorizationTest extends TestCase
|
||||
{
|
||||
public function test_superuser_can_mount()
|
||||
{
|
||||
$this->actingAs(User::factory()->superuser()->create());
|
||||
|
||||
Livewire::test(LocationScopeCheck::class)
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_non_superuser_cannot_mount_or_replay()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(LocationScopeCheck::class)
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
157
tests/Feature/Livewire/OauthClientsAuthorizationTest.php
Normal file
157
tests/Feature/Livewire/OauthClientsAuthorizationTest.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\OauthClients;
|
||||
use App\Models\User;
|
||||
use Laravel\Passport\Client;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the Livewire snapshot-replay authorization bypass
|
||||
* reported by PizzaStev3 (2026-07-31). Route-level middleware on /admin/oauth
|
||||
* enforces superuser but a POST /livewire/update with a valid signed snapshot
|
||||
* of the oauth-clients component reached the component's methods regardless.
|
||||
*
|
||||
* Fix: boot() gates the admin section, individual sensitive methods
|
||||
* (createClient, deleteAuthorizedApplication, editClient) re-check, and
|
||||
* the section property is #[Locked] so a client-side snapshot cannot flip
|
||||
* from admin section to user section to slip past boot().
|
||||
*/
|
||||
class OauthClientsAuthorizationTest extends TestCase
|
||||
{
|
||||
public function test_superuser_can_mount_the_admin_oauth_clients_section()
|
||||
{
|
||||
$this->actingAs(User::factory()->superuser()->create());
|
||||
|
||||
Livewire::test(OauthClients::class, ['section' => 'oauth-clients'])
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_non_superuser_cannot_mount_the_admin_oauth_clients_section()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(OauthClients::class, ['section' => 'oauth-clients'])
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_non_superuser_cannot_replay_create_client_via_authorized_applications_section()
|
||||
{
|
||||
// The section property is Locked so a client can't rewrite it from
|
||||
// authorized-applications to oauth-clients to slip past boot().
|
||||
// Even if boot() lets them through under a benign section, the
|
||||
// per-method superuser check on createClient blocks the mint.
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(OauthClients::class, ['section' => 'authorized-applications'])
|
||||
->set('name', 'attacker-client')
|
||||
->set('redirect', 'http://attacker.test/callback')
|
||||
->call('createClient')
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseMissing('oauth_clients', ['name' => 'attacker-client']);
|
||||
}
|
||||
|
||||
public function test_non_superuser_cannot_replay_delete_authorized_application_for_others_tokens()
|
||||
{
|
||||
$victim = User::factory()->create();
|
||||
$client = Client::create([
|
||||
'user_id' => $victim->id,
|
||||
'name' => 'Victim App',
|
||||
'secret' => 'secret',
|
||||
'provider' => null,
|
||||
'redirect' => 'http://victim.test/callback',
|
||||
'personal_access_client' => false,
|
||||
'password_client' => false,
|
||||
'revoked' => false,
|
||||
]);
|
||||
$victimTokenId = 'victim-token-'.uniqid();
|
||||
\DB::table('oauth_access_tokens')->insert([
|
||||
'id' => $victimTokenId,
|
||||
'user_id' => $victim->id,
|
||||
'client_id' => $client->id,
|
||||
'name' => 'victim',
|
||||
'scopes' => '[]',
|
||||
'revoked' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
]);
|
||||
|
||||
// Attacker acts under the authorized-applications section (which a
|
||||
// non-superuser IS allowed to access, since it's the account/api
|
||||
// surface). Replay attempts to revoke the victim's active token.
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(OauthClients::class, ['section' => 'authorized-applications'])
|
||||
->call('deleteAuthorizedApplication', $client->id);
|
||||
|
||||
$this->assertDatabaseHas('oauth_access_tokens', [
|
||||
'id' => $victimTokenId,
|
||||
'revoked' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_superuser_can_still_revoke_any_authorized_application()
|
||||
{
|
||||
$victim = User::factory()->create();
|
||||
$client = Client::create([
|
||||
'user_id' => $victim->id,
|
||||
'name' => 'Victim App',
|
||||
'secret' => 'secret',
|
||||
'provider' => null,
|
||||
'redirect' => 'http://victim.test/callback',
|
||||
'personal_access_client' => false,
|
||||
'password_client' => false,
|
||||
'revoked' => false,
|
||||
]);
|
||||
$victimTokenId = 'victim-token-'.uniqid();
|
||||
\DB::table('oauth_access_tokens')->insert([
|
||||
'id' => $victimTokenId,
|
||||
'user_id' => $victim->id,
|
||||
'client_id' => $client->id,
|
||||
'name' => 'victim',
|
||||
'scopes' => '[]',
|
||||
'revoked' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
]);
|
||||
|
||||
$this->actingAs(User::factory()->superuser()->create());
|
||||
|
||||
Livewire::test(OauthClients::class, ['section' => 'oauth-clients'])
|
||||
->call('deleteAuthorizedApplication', $client->id);
|
||||
|
||||
$this->assertDatabaseHas('oauth_access_tokens', [
|
||||
'id' => $victimTokenId,
|
||||
'revoked' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_non_superuser_cannot_replay_edit_client_to_read_other_client_details()
|
||||
{
|
||||
$victim = User::factory()->create();
|
||||
$client = Client::create([
|
||||
'user_id' => $victim->id,
|
||||
'name' => 'Victim App',
|
||||
'secret' => 'secret',
|
||||
'provider' => null,
|
||||
'redirect' => 'http://victim.test/callback',
|
||||
'personal_access_client' => false,
|
||||
'password_client' => false,
|
||||
'revoked' => false,
|
||||
]);
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$component = Livewire::test(OauthClients::class, ['section' => 'authorized-applications'])
|
||||
->call('editClient', $client->id)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertSame('', (string) $component->get('editName'));
|
||||
$this->assertSame('', (string) $component->get('editRedirect'));
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ class PersonalAccessTokensTest extends TestCase
|
||||
{
|
||||
public function test_the_component_can_render()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
$this->actingAs(User::factory()->selfApi()->create());
|
||||
|
||||
Livewire::test(PersonalAccessTokens::class)
|
||||
->assertStatus(200);
|
||||
@ -19,11 +19,30 @@ class PersonalAccessTokensTest extends TestCase
|
||||
|
||||
public function test_create_token_validation_fails_without_name()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
$this->actingAs(User::factory()->selfApi()->create());
|
||||
|
||||
Livewire::test(PersonalAccessTokens::class)
|
||||
->set('name', '')
|
||||
->call('createToken')
|
||||
->assertHasErrors(['name' => 'required']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression for the Livewire snapshot-replay class of vuln reported
|
||||
* by PizzaStev3 (2026-07-31). Without the boot() gate, a user who was
|
||||
* blocked from /account/api by the self.api middleware could still
|
||||
* mint a PAT by replaying a valid signed snapshot of the
|
||||
* PersonalAccessTokens component to POST /livewire/update.
|
||||
*
|
||||
* boot() fires on both the initial mount AND every subsequent
|
||||
* /livewire/update, so a 403 at mount here implies the same 403 on any
|
||||
* replayed action call.
|
||||
*/
|
||||
public function test_user_without_self_api_permission_cannot_mount_or_replay_the_component()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(PersonalAccessTokens::class)
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\SlackSettingsForm;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the Livewire snapshot-replay authorization bypass
|
||||
* reported by PizzaStev3 (2026-07-31). SlackSettingsForm had no
|
||||
* per-request authorization and exposed webhook mutation methods
|
||||
* (testWebhook, clearSettings, submit) plus render()-time disclosure of
|
||||
* the configured webhook_endpoint/channel. boot() gate now blocks both
|
||||
* mount and any replay under a non-superuser session.
|
||||
*/
|
||||
class SlackSettingsFormAuthorizationTest extends TestCase
|
||||
{
|
||||
public function test_superuser_can_mount()
|
||||
{
|
||||
$this->actingAs(User::factory()->superuser()->create());
|
||||
|
||||
Livewire::test(SlackSettingsForm::class)
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_non_superuser_cannot_mount_or_replay()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(SlackSettingsForm::class)
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user