diff --git a/app/Models/Category.php b/app/Models/Category.php index fb03bec..e72c179 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -9,12 +9,10 @@ class Category extends Model { protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/City.php b/app/Models/City.php index 5536723..9fe5ad7 100644 --- a/app/Models/City.php +++ b/app/Models/City.php @@ -17,12 +17,10 @@ class City extends Model protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/Country.php b/app/Models/Country.php index a4f4898..4ea5af0 100644 --- a/app/Models/Country.php +++ b/app/Models/Country.php @@ -9,12 +9,10 @@ class Country extends Model { protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/Course.php b/app/Models/Course.php index 1b346c1..e9205c2 100644 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -19,12 +19,11 @@ class Course extends Model implements HasMedia protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + 'description', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/CourseEvent.php b/app/Models/CourseEvent.php index c9ed68d..668b501 100644 --- a/app/Models/CourseEvent.php +++ b/app/Models/CourseEvent.php @@ -9,12 +9,11 @@ class CourseEvent extends Model { protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'from', + 'to', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/EinundzwanzigPleb.php b/app/Models/EinundzwanzigPleb.php index fefbdcd..d012b46 100644 --- a/app/Models/EinundzwanzigPleb.php +++ b/app/Models/EinundzwanzigPleb.php @@ -15,7 +15,17 @@ class EinundzwanzigPleb extends Authenticatable implements CipherSweetEncrypted use HasFactory; use UsesCipherSweet; - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'npub', + 'pubkey', + 'email', + 'no_email', + 'nip05_handle', + 'association_status', + 'application_text', + 'archived_application_text', + ]; protected function casts(): array { diff --git a/app/Models/Election.php b/app/Models/Election.php index 7f713cd..6ba9e6a 100644 --- a/app/Models/Election.php +++ b/app/Models/Election.php @@ -9,7 +9,8 @@ class Election extends Model { use HasFactory; - protected $guarded = []; + /** @var list */ + protected $fillable = []; protected function casts(): array { diff --git a/app/Models/Event.php b/app/Models/Event.php index 7b2b55c..72cb13c 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -6,7 +6,14 @@ use Illuminate\Database\Eloquent\Model; class Event extends Model { - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'event_id', + 'pubkey', + 'parent_event_id', + 'json', + 'type', + ]; public function renderedEvent() { diff --git a/app/Models/Lecturer.php b/app/Models/Lecturer.php index f780dbe..f333ea6 100644 --- a/app/Models/Lecturer.php +++ b/app/Models/Lecturer.php @@ -21,12 +21,10 @@ class Lecturer extends Model implements HasMedia protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/Meetup.php b/app/Models/Meetup.php index c458c42..e8f5ef8 100644 --- a/app/Models/Meetup.php +++ b/app/Models/Meetup.php @@ -21,12 +21,10 @@ class Meetup extends Model implements HasMedia protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/MeetupEvent.php b/app/Models/MeetupEvent.php index 52d1617..888b44d 100644 --- a/app/Models/MeetupEvent.php +++ b/app/Models/MeetupEvent.php @@ -9,12 +9,10 @@ class MeetupEvent extends Model { protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'start', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/Notification.php b/app/Models/Notification.php index 3364b11..6bb344e 100644 --- a/app/Models/Notification.php +++ b/app/Models/Notification.php @@ -12,7 +12,11 @@ class Notification extends Model implements HasMedia { use InteractsWithMedia; - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + 'description', + ]; protected function casts(): array { diff --git a/app/Models/PaymentEvent.php b/app/Models/PaymentEvent.php index ea62194..601f121 100644 --- a/app/Models/PaymentEvent.php +++ b/app/Models/PaymentEvent.php @@ -9,7 +9,14 @@ class PaymentEvent extends Model { use HasFactory; - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'year', + 'event_id', + 'amount', + 'paid', + 'btc_pay_invoice', + ]; public function pleb() { diff --git a/app/Models/Profile.php b/app/Models/Profile.php index 4628de8..26321ee 100644 --- a/app/Models/Profile.php +++ b/app/Models/Profile.php @@ -6,5 +6,18 @@ use Illuminate\Database\Eloquent\Model; class Profile extends Model { - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'pubkey', + 'name', + 'display_name', + 'picture', + 'banner', + 'website', + 'about', + 'nip05', + 'lud16', + 'lud06', + 'deleted', + ]; } diff --git a/app/Models/ProjectProposal.php b/app/Models/ProjectProposal.php index 7f5f008..20f3f1c 100644 --- a/app/Models/ProjectProposal.php +++ b/app/Models/ProjectProposal.php @@ -20,12 +20,13 @@ class ProjectProposal extends Model implements HasMedia use HasSlug; use InteractsWithMedia; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + 'description', + 'support_in_sats', + 'website', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/RenderedEvent.php b/app/Models/RenderedEvent.php index e525cc4..21be49c 100644 --- a/app/Models/RenderedEvent.php +++ b/app/Models/RenderedEvent.php @@ -6,7 +6,13 @@ use Illuminate\Database\Eloquent\Model; class RenderedEvent extends Model { - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'event_id', + 'html', + 'profile_image', + 'profile_name', + ]; public function event() { diff --git a/app/Models/Venue.php b/app/Models/Venue.php index 557f2af..aadf1a7 100644 --- a/app/Models/Venue.php +++ b/app/Models/Venue.php @@ -22,12 +22,10 @@ class Venue extends Model implements HasMedia protected $connection = 'einundzwanzig'; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'name', + ]; /** * The attributes that should be cast to native types. diff --git a/app/Models/Vote.php b/app/Models/Vote.php index 02cbb72..7cd8576 100644 --- a/app/Models/Vote.php +++ b/app/Models/Vote.php @@ -7,12 +7,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class Vote extends Model { - /** - * The attributes that aren't mass assignable. - * - * @var array - */ - protected $guarded = []; + /** @var list */ + protected $fillable = [ + 'einundzwanzig_pleb_id', + 'project_proposal_id', + 'value', + 'reason', + ]; /** * The attributes that should be cast to native types. diff --git a/resources/views/livewire/association/news.blade.php b/resources/views/livewire/association/news.blade.php index 4df9487..d2b25af 100644 --- a/resources/views/livewire/association/news.blade.php +++ b/resources/views/livewire/association/news.blade.php @@ -106,9 +106,10 @@ class extends Component { $news = Notification::query()->create([ 'name' => $this->form['name'], 'description' => $this->form['description'] ?? null, - 'category' => $this->form['category'], - 'einundzwanzig_pleb_id' => $currentPleb->id, ]); + $news->category = $this->form['category']; + $news->einundzwanzig_pleb_id = $currentPleb->id; + $news->save(); if ($this->file) { $news diff --git a/resources/views/livewire/association/project-support/form/create.blade.php b/resources/views/livewire/association/project-support/form/create.blade.php index f75e9f6..a75a160 100644 --- a/resources/views/livewire/association/project-support/form/create.blade.php +++ b/resources/views/livewire/association/project-support/form/create.blade.php @@ -71,10 +71,11 @@ class extends Component 'description' => $this->form['description'], 'support_in_sats' => (int) $this->form['support_in_sats'], 'website' => $this->form['website'], - 'accepted' => $this->form['accepted'], - 'sats_paid' => $this->form['sats_paid'], - 'einundzwanzig_pleb_id' => \App\Models\EinundzwanzigPleb::query()->where('pubkey', NostrAuth::pubkey())->first()->id, ]); + $projectProposal->accepted = $this->form['accepted']; + $projectProposal->sats_paid = $this->form['sats_paid']; + $projectProposal->einundzwanzig_pleb_id = \App\Models\EinundzwanzigPleb::query()->where('pubkey', NostrAuth::pubkey())->first()->id; + $projectProposal->save(); if ($this->file) { $projectProposal->addMedia($this->file)->toMediaCollection('main'); diff --git a/tests/Feature/MassAssignmentProtectionTest.php b/tests/Feature/MassAssignmentProtectionTest.php new file mode 100644 index 0000000..9d22642 --- /dev/null +++ b/tests/Feature/MassAssignmentProtectionTest.php @@ -0,0 +1,288 @@ +getProperty('guarded'); + $instance = $reflection->newInstanceWithoutConstructor(); + $guarded = $property->getValue($instance); + + expect($guarded) + ->not->toBe([], "{$modelClass} still uses \$guarded = [] which is insecure"); + } +}); + +it('ensures all models have explicit fillable arrays', function () { + $models = [ + PaymentEvent::class, + EinundzwanzigPleb::class, + Vote::class, + ProjectProposal::class, + Election::class, + Venue::class, + MeetupEvent::class, + CourseEvent::class, + Course::class, + Meetup::class, + Lecturer::class, + City::class, + Event::class, + RenderedEvent::class, + Profile::class, + Category::class, + Country::class, + Notification::class, + ]; + + foreach ($models as $modelClass) { + $reflection = new ReflectionClass($modelClass); + $property = $reflection->getProperty('fillable'); + $instance = $reflection->newInstanceWithoutConstructor(); + + expect($property->getValue($instance)) + ->toBeArray("{$modelClass} should have an explicit \$fillable array"); + } +}); + +it('blocks mass assignment of einundzwanzig_pleb_id on PaymentEvent', function () { + $paymentEvent = new PaymentEvent; + $paymentEvent->fill(['einundzwanzig_pleb_id' => 999]); + + expect($paymentEvent->einundzwanzig_pleb_id)->toBeNull(); +}); + +it('verifies EinundzwanzigPleb fillable does not contain application_for', function () { + $reflection = new ReflectionClass(EinundzwanzigPleb::class); + $property = $reflection->getProperty('fillable'); + $instance = $reflection->newInstanceWithoutConstructor(); + $fillable = $property->getValue($instance); + + expect($fillable)->not->toContain('application_for'); + expect($fillable)->not->toContain('id'); + expect($fillable)->toContain('npub'); + expect($fillable)->toContain('pubkey'); + expect($fillable)->toContain('email'); + expect($fillable)->toContain('no_email'); + expect($fillable)->toContain('nip05_handle'); +}); + +it('blocks mass assignment of accepted and sats_paid on ProjectProposal', function () { + $proposal = new ProjectProposal; + $proposal->fill([ + 'name' => 'Test', + 'accepted' => true, + 'sats_paid' => 100000, + 'einundzwanzig_pleb_id' => 1, + 'slug' => 'injected-slug', + ]); + + expect($proposal->accepted)->toBeNull(); + expect($proposal->sats_paid)->toBeNull(); + expect($proposal->einundzwanzig_pleb_id)->toBeNull(); + expect($proposal->slug)->toBeNull(); + expect($proposal->name)->toBe('Test'); +}); + +it('blocks mass assignment of all fields on Election', function () { + $election = new Election; + + expect(fn () => $election->fill(['year' => 2025])) + ->toThrow(MassAssignmentException::class); +}); + +it('blocks mass assignment of created_by and slug on Venue', function () { + $venue = new Venue; + $venue->fill([ + 'name' => 'Test Venue', + 'created_by' => 999, + 'slug' => 'injected-slug', + ]); + + expect($venue->name)->toBe('Test Venue'); + expect($venue->created_by)->toBeNull(); + expect($venue->slug)->toBeNull(); +}); + +it('blocks mass assignment of meetup_id and created_by on MeetupEvent', function () { + $event = new MeetupEvent; + $event->fill([ + 'start' => '2025-01-01', + 'meetup_id' => 999, + 'created_by' => 999, + 'attendees' => ['a'], + ]); + + expect($event->start)->not->toBeNull(); + expect($event->meetup_id)->toBeNull(); + expect($event->created_by)->toBeNull(); + expect($event->attendees)->toBeNull(); +}); + +it('blocks mass assignment of course_id venue_id and created_by on CourseEvent', function () { + $event = new CourseEvent; + $event->fill([ + 'from' => '2025-01-01', + 'to' => '2025-01-02', + 'course_id' => 999, + 'venue_id' => 999, + 'created_by' => 999, + ]); + + expect($event->from)->not->toBeNull(); + expect($event->to)->not->toBeNull(); + expect($event->course_id)->toBeNull(); + expect($event->venue_id)->toBeNull(); + expect($event->created_by)->toBeNull(); +}); + +it('blocks mass assignment of lecturer_id and created_by on Course', function () { + $course = new Course; + $course->fill([ + 'name' => 'Test Course', + 'description' => 'Test', + 'lecturer_id' => 999, + 'created_by' => 999, + ]); + + expect($course->name)->toBe('Test Course'); + expect($course->description)->toBe('Test'); + expect($course->lecturer_id)->toBeNull(); + expect($course->created_by)->toBeNull(); +}); + +it('blocks mass assignment of city_id created_by and slug on Meetup', function () { + $meetup = new Meetup; + $meetup->fill([ + 'name' => 'Test Meetup', + 'city_id' => 999, + 'created_by' => 999, + 'slug' => 'injected', + 'github_data' => '{}', + 'simplified_geojson' => '{}', + ]); + + expect($meetup->name)->toBe('Test Meetup'); + expect($meetup->city_id)->toBeNull(); + expect($meetup->created_by)->toBeNull(); + expect($meetup->slug)->toBeNull(); +}); + +it('blocks mass assignment of active created_by and slug on Lecturer', function () { + $lecturer = new Lecturer; + $lecturer->fill([ + 'name' => 'Test Lecturer', + 'active' => true, + 'created_by' => 999, + 'slug' => 'injected', + ]); + + expect($lecturer->name)->toBe('Test Lecturer'); + expect($lecturer->active)->toBeNull(); + expect($lecturer->created_by)->toBeNull(); + expect($lecturer->slug)->toBeNull(); +}); + +it('blocks mass assignment of country_id created_by and slug on City', function () { + $city = new City; + $city->fill([ + 'name' => 'Test City', + 'country_id' => 999, + 'created_by' => 999, + 'slug' => 'injected', + 'osm_relation' => '{}', + 'simplified_geojson' => '{}', + ]); + + expect($city->name)->toBe('Test City'); + expect($city->country_id)->toBeNull(); + expect($city->created_by)->toBeNull(); + expect($city->slug)->toBeNull(); +}); + +it('blocks mass assignment of einundzwanzig_pleb_id and category on Notification', function () { + $notification = new Notification; + $notification->fill([ + 'name' => 'Test News', + 'description' => 'Test', + 'einundzwanzig_pleb_id' => 999, + 'category' => 1, + ]); + + expect($notification->name)->toBe('Test News'); + expect($notification->description)->toBe('Test'); + expect($notification->einundzwanzig_pleb_id)->toBeNull(); + expect($notification->category)->toBeNull(); +}); + +it('blocks mass assignment of code and language_codes on Country', function () { + $country = new Country; + $country->fill([ + 'name' => 'Test', + 'code' => 'XX', + 'language_codes' => ['en'], + ]); + + expect($country->name)->toBe('Test'); + expect($country->code)->toBeNull(); + expect($country->language_codes)->toBeNull(); +}); + +it('allows fillable fields on PaymentEvent', function () { + $paymentEvent = new PaymentEvent; + $paymentEvent->fill([ + 'year' => 2025, + 'event_id' => 'test-event', + 'amount' => 21000, + 'paid' => true, + 'btc_pay_invoice' => 'inv-123', + ]); + + expect($paymentEvent->year)->toBe(2025); + expect($paymentEvent->event_id)->toBe('test-event'); + expect($paymentEvent->amount)->toBe(21000); + expect($paymentEvent->paid)->toBeTrue(); + expect($paymentEvent->btc_pay_invoice)->toBe('inv-123'); +});