3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/tests/Feature/Assets/Ui/EncryptedCustomFieldFormDisclosureTest.php
2026-07-29 14:49:11 +01:00

197 lines
7.8 KiB
PHP

<?php
namespace Tests\Feature\Assets\Ui;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Tests\TestCase;
/**
* FD-56673 regression coverage. Asserts that every element-type branch in
* resources/views/models/custom_fields_form.blade.php honors the
* assets.view.encrypted_custom_fields gate, and that the fix is picked up
* on each of the four form contexts that include the template
* (edit, checkin, checkout, audit).
*
* The pre-fix template unconditionally called Helper::gracefulDecrypt on
* the listbox, textarea, markdown-textarea, date_picker, datetime_picker,
* DATE-format, and DATETIME-format branches, leaking plaintext to any
* assets.edit / assets.checkin / assets.checkout / assets.audit user.
*/
class EncryptedCustomFieldFormDisclosureTest extends TestCase
{
/**
* @dataProvider elementTypeProvider
*/
public function test_edit_form_masks_encrypted_custom_field_for_user_without_view_encrypted(array $fieldAttributes, string $secret): void
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
$asset = $this->assetWithEncryptedField($fieldAttributes, $secret);
$actor = User::factory()->editAssets()->create();
$this->actingAs($actor)
->get(route('hardware.edit', $asset))
->assertOk()
->assertDontSee($secret, false)
->assertSee(strtoupper(trans('admin/custom_fields/general.encrypted')), false);
}
/**
* @dataProvider elementTypeProvider
*/
public function test_edit_form_shows_encrypted_custom_field_plaintext_for_superuser(array $fieldAttributes, string $secret): void
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
$asset = $this->assetWithEncryptedField($fieldAttributes, $secret);
$actor = User::factory()->superuser()->create();
$this->actingAs($actor)
->get(route('hardware.edit', $asset))
->assertOk()
->assertSee($secret, false);
}
public function test_checkin_form_masks_encrypted_custom_field_for_user_without_view_encrypted(): void
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
// Checkin flow requires the asset to be checked out first, otherwise the
// controller redirects to hardware.index with "already checked in".
$asset = $this->assetWithEncryptedField(
['element' => 'textarea', 'display_checkin' => 1],
'CHECKIN-SECRET-SHOULD-NOT-LEAK',
fn ($factory) => $factory->assignedToUser()
);
$actor = User::factory()->checkinAssets()->create();
$response = $this->actingAs($actor)->get(route('hardware.checkin.create', $asset))->assertOk();
$response->assertDontSee('CHECKIN-SECRET-SHOULD-NOT-LEAK', false);
$response->assertSee(strtoupper(trans('admin/custom_fields/general.encrypted')), false);
}
public function test_checkout_form_masks_encrypted_custom_field_for_user_without_view_encrypted(): void
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
$asset = $this->assetWithEncryptedField(
['element' => 'textarea', 'display_checkout' => 1],
'CHECKOUT-SECRET-SHOULD-NOT-LEAK'
);
$actor = User::factory()->checkoutAssets()->create();
$response = $this->actingAs($actor)->get(route('hardware.checkout.create', $asset))->assertOk();
$response->assertDontSee('CHECKOUT-SECRET-SHOULD-NOT-LEAK', false);
$response->assertSee(strtoupper(trans('admin/custom_fields/general.encrypted')), false);
}
public function test_audit_form_masks_encrypted_custom_field_for_user_without_view_encrypted(): void
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
$asset = $this->assetWithEncryptedField(
['element' => 'textarea', 'display_audit' => 1],
'AUDIT-SECRET-SHOULD-NOT-LEAK'
);
$actor = User::factory()->auditAssets()->create();
$response = $this->actingAs($actor)->get(route('asset.audit.create', $asset))->assertOk();
$response->assertDontSee('AUDIT-SECRET-SHOULD-NOT-LEAK', false);
$response->assertSee(strtoupper(trans('admin/custom_fields/general.encrypted')), false);
}
/**
* Each row exercises one of the seven Blade branches that used to leak
* plaintext when the caller lacked view.encrypted_custom_fields.
*/
public static function elementTypeProvider(): array
{
return [
// Note: element='listbox' is intentionally excluded from this
// feature-level provider. The <x-input.select> component only
// renders options that appear in field_values, so both the
// plaintext (pre-fix) and the mask (post-fix) collapse into
// "no option marked selected" when they do not match one of
// the configured options. There is no substring assertion that
// cleanly distinguishes the two states without regex parsing.
// The listbox branch calls Helper::customFieldFormValue verbatim
// (identical to the four branches below), and HelperTest covers
// the helper's return values directly.
'textarea' => [
['element' => 'textarea'],
'SECRET-INSIDE-TEXTAREA',
],
'markdown-textarea' => [
['element' => 'markdown-textarea'],
'SECRET-INSIDE-MARKDOWN-TEXTAREA',
],
'date_picker' => [
['element' => 'date_picker'],
'2026-07-29',
],
'datetime_picker' => [
['element' => 'datetime_picker'],
'2026-07-29 13:37:00',
],
// Note: element='text' with format='DATE' or 'DATETIME' also render
// decrypt chains in the template, but CustomField::canEncryptFor()
// rejects field_encrypted='1' for those formats at validation time
// (native date columns can't hold ciphertext), so those branches
// can never actually render an encrypted value. Not vulnerable.
];
}
/**
* Build an asset whose model uses a fieldset containing a single encrypted
* custom field with the given attributes and stored plaintext $secret.
*
* Custom fields materialize their `db_column` and add a real database
* column via an afterCreating observer, so we always re-fetch the field
* from the DB after the factory returns rather than trusting the in-memory
* factory instance (which will not know its own db_column yet).
*
* Optionally accepts a $factoryState closure to layer additional Asset
* factory states (e.g. assignedToUser() for the checkin test).
*/
private function assetWithEncryptedField(array $fieldAttributes, string $secret, ?callable $factoryState = null): Asset
{
$uniqueName = 'Encrypted Field '.uniqid();
CustomField::factory()->create(array_merge([
'name' => $uniqueName,
'field_encrypted' => '1',
], $fieldAttributes));
$field = CustomField::where('name', $uniqueName)->firstOrFail();
$fieldset = CustomFieldset::factory()->create();
$fieldset->fields()->attach($field, ['order' => '1', 'required' => false]);
$model = AssetModel::factory()->create(['fieldset_id' => $fieldset->id]);
$assetFactory = Asset::factory()->state([
'model_id' => $model->id,
$field->db_column => Crypt::encrypt($secret),
]);
if ($factoryState) {
$assetFactory = $factoryState($assetFactory);
}
return $assetFactory->create();
}
}