mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-02-15 03:23:17 +00:00
## Security Audit: Mass Assignment Protection ### Problem Alle 18 Eloquent Models verwenden `protected $guarded = [];` – das bedeutet **kein Schutz** gegen Mass Assignment. Ein Angreifer könnte über manipulierte Requests sensible Felder wie `accepted`, `sats_paid`, `association_status`, `paid` oder `created_by` direkt setzen. ### Betroffene Dateien und empfohlene Änderungen Ersetze in **jedem** der folgenden Models `protected $guarded = [];` durch ein explizites `protected $fillable = [...]` Array. Hier die Analyse pro Model: **Höchstes Risiko (Finanzen & Identity):** 1. **`app/Models/PaymentEvent.php`** – Finanz-kritisch! - Sensible Felder (NICHT fillable): `einundzwanzig_pleb_id`, `year`, `amount`, `event_id`, `paid`, `btc_pay_invoice` - `$fillable` sollte leer oder minimal sein – alle Felder werden programmatisch gesetzt 2. **`app/Models/EinundzwanzigPleb.php`** - Sensible Felder: `association_status`, `application_for`, `nip05_handle` - `$fillable = ['npub', 'pubkey', 'email', 'no_email', 'application_text', 'archived_application_text']` 3. **`app/Models/Vote.php`** - Sensible Felder: `einundzwanzig_pleb_id`, `project_proposal_id`, `value` - `$fillable = ['reason']` – alle anderen Felder müssen programmatisch gesetzt werden 4. **`app/Models/ProjectProposal.php`** - Sensible Felder: `einundzwanzig_pleb_id`, `accepted`, `sats_paid`, `slug` - `$fillable = ['name', 'support_in_sats', 'description', 'website']` 5. **`app/Models/Election.php`** - Sensible Felder: `year`, `candidates`, `end_time` - `$fillable` sollte leer sein – nur Admin-gesteuert **Mittleres Risiko (mit `created_by` auto-fill in boot):** 6. **`app/Models/Venue.php`** – `$fillable = ['name']` (slug & created_by auto-generiert) 7. **`app/Models/MeetupEvent.php`** – `$fillable = ['start']` (meetup_id, created_by, attendees guarded) 8. **`app/Models/CourseEvent.php`** – `$fillable = ['from', 'to']` (course_id, venue_id, created_by guarded) 9. **`app/Models/Course.php`** – `$fillable = ['name', 'description']` (lecturer_id, created_by guarded) 10. **`app/Models/Meetup.php`** – `$fillable = ['name']` (city_id, created_by, slug, github_data, simplified_geojson guarded) 11. **`app/Models/Lecturer.php`** – `$fillable = ['name']` (active, created_by, slug guarded) 12. **`app/Models/City.php`** – `$fillable = ['name']` (country_id, created_by, slug, osm_relation, simplified_geojson guarded) **Niedrigeres Risiko (Lookup/Reference-Daten):** 13. **`app/Models/Event.php`** – `$fillable = []` (alle Felder: event_id, parent_event_id, pubkey, json, type sind extern gesteuert) 14. **`app/Models/RenderedEvent.php`** – `$fillable = []` (event_id, html, profile_image, profile_name alle system-generiert) 15. **`app/Models/Profile.php`** – `$fillable = ['name', 'display_name', 'picture', 'banner', 'website', 'about']` (pubkey, deleted, nip05, lud16, lud06 guarded) 16. **`app/Models/Category.php`** – `$fillable = ['name']` 17. **`app/Models/Country.php`** – `$fillable = ['name']` (code, language_codes guarded) 18. **`app/Models/Notification.php`** – `$fillable = ['name', 'description']` (einundzwanzig_pleb_id, category guarded) ### Vorgehen 1. Jedes Model öffnen und `$guarded = []` durch das oben definierte `$fillable` Array ersetzen 2. Prüfen, ob bestehende `::create()` oder `::update()` Aufrufe noch funktionieren – ggf. müssen explizite Feld-Zuweisungen ergänzt werden 3. Für jedes geänderte Model einen Pest-Test schreiben, der verifiziert, dass Mass Assignment von sensiblen Feldern blockiert wird 4. `vendor/bin/pint --dirty` ausführen 5. Bestehende Tests laufen lassen: `php artisan test --compact` ### Akzeptanzkriterien - Kein Model hat mehr `$guarded = []` - Alle sensiblen Felder (status, paid, accepted, created_by, slug, IDs) sind NICHT in `$fillable` - Bestehende Features funktionieren weiterhin (Tests grün) - Neue Tests verifizieren Mass Assignment Protection
120 lines
3.1 KiB
PHP
120 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Support\Facades\Cookie;
|
|
use Spatie\Image\Enums\Fit;
|
|
use Spatie\MediaLibrary\HasMedia;
|
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
|
use Spatie\Sluggable\HasSlug;
|
|
use Spatie\Sluggable\SlugOptions;
|
|
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
|
|
|
|
class Venue extends Model implements HasMedia
|
|
{
|
|
use HasRelationships;
|
|
use HasSlug;
|
|
use InteractsWithMedia;
|
|
|
|
protected $connection = 'einundzwanzig';
|
|
|
|
/** @var list<string> */
|
|
protected $fillable = [
|
|
'name',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be cast to native types.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $casts = [
|
|
'id' => 'integer',
|
|
'city_id' => 'integer',
|
|
];
|
|
|
|
protected static function booted()
|
|
{
|
|
static::creating(function ($model) {
|
|
if (! $model->created_by) {
|
|
$model->created_by = auth()->id();
|
|
}
|
|
});
|
|
}
|
|
|
|
public function registerMediaConversions(?Media $media = null): void
|
|
{
|
|
$this
|
|
->addMediaConversion('preview')
|
|
->fit(Fit::Crop, 300, 300)
|
|
->nonQueued();
|
|
$this->addMediaConversion('thumb')
|
|
->fit(Fit::Crop, 130, 130)
|
|
->width(130)
|
|
->height(130);
|
|
}
|
|
|
|
public function registerMediaCollections(): void
|
|
{
|
|
$this->addMediaCollection('images')
|
|
->acceptsMimeTypes([
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/gif',
|
|
'image/webp',
|
|
])
|
|
->useDisk('private')
|
|
->useFallbackUrl(asset('img/einundzwanzig.png'));
|
|
}
|
|
|
|
public function getSignedMediaUrl(string $collection = 'images', int $expireMinutes = 60): string
|
|
{
|
|
$media = $this->getFirstMedia($collection);
|
|
if (! $media) {
|
|
return asset('img/einundzwanzig.png');
|
|
}
|
|
|
|
return url()->temporarySignedRoute('media.signed', now()->addMinutes($expireMinutes), ['media' => $media]);
|
|
}
|
|
|
|
/**
|
|
* Get the options for generating the slug.
|
|
*/
|
|
public function getSlugOptions(): SlugOptions
|
|
{
|
|
return SlugOptions::create()
|
|
->generateSlugsFrom(['city.slug', 'name'])
|
|
->saveSlugsTo('slug')
|
|
->usingLanguage(Cookie::get('lang', config('app.locale')));
|
|
}
|
|
|
|
public function createdBy(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'created_by');
|
|
}
|
|
|
|
public function city(): BelongsTo
|
|
{
|
|
return $this->belongsTo(City::class);
|
|
}
|
|
|
|
public function lecturers()
|
|
{
|
|
return $this->hasManyDeepFromRelations($this->courses(), (new Course)->lecturer());
|
|
}
|
|
|
|
public function courses()
|
|
{
|
|
return $this->hasManyDeepFromRelations($this->events(), (new CourseEvent)->course());
|
|
}
|
|
|
|
public function courseEvents(): HasMany
|
|
{
|
|
return $this->hasMany(CourseEvent::class);
|
|
}
|
|
}
|