29 - Security hardening: registration gate, input validation, nginx headers, env defaults, user model

This commit is contained in:
myrmidex 2026-05-02 16:14:31 +02:00
parent 27f0ac8568
commit b1d0ab793c
9 changed files with 39 additions and 24 deletions

View file

@ -1,7 +1,7 @@
APP_NAME=Laravel APP_NAME=Laravel
APP_ENV=local APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=false
APP_URL=http://localhost APP_URL=http://localhost
APP_LOCALE=en APP_LOCALE=en
@ -18,18 +18,18 @@ BCRYPT_ROUNDS=12
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_STACK=single LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=error
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=db DB_HOST=db
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=incr DB_DATABASE=incr
DB_USERNAME=incr_user DB_USERNAME=incr_user
DB_PASSWORD=incr_password DB_PASSWORD=change_me_in_production
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false SESSION_ENCRYPT=true
SESSION_PATH=/ SESSION_PATH=/
SESSION_DOMAIN=null SESSION_DOMAIN=null

View file

@ -21,6 +21,10 @@ class RegisteredUserController extends Controller
*/ */
public function create(): Response public function create(): Response
{ {
if (User::exists()) {
abort(403, 'Registration is disabled.');
}
return Inertia::render('auth/register'); return Inertia::render('auth/register');
} }
@ -31,16 +35,20 @@ public function create(): Response
*/ */
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
$request->validate([ if (User::exists()) {
abort(403, 'Registration is disabled.');
}
$validated = $request->validate([
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()], 'password' => ['required', 'confirmed', Rules\Password::defaults()],
]); ]);
$user = User::create([ $user = User::forceCreate([
'name' => $request->name, 'name' => $validated['name'],
'email' => $request->email, 'email' => $validated['email'],
'password' => Hash::make($request->password), 'password' => Hash::make($validated['password']),
]); ]);
event(new Registered($user)); event(new Registered($user));

View file

@ -12,15 +12,12 @@ class MilestoneController extends Controller
{ {
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
$request->validate([ $validated = $request->validate([
'target' => 'required|integer|min:1', 'target' => 'required|integer|min:1',
'description' => 'required|string|max:255', 'description' => 'required|string|max:255',
]); ]);
Milestone::create([ Milestone::create($validated);
'target' => $request->target,
'description' => $request->description,
]);
return back()->with('success', 'Milestone created successfully'); return back()->with('success', 'Milestone created successfully');
} }

View file

@ -46,13 +46,15 @@ public function update(Request $request)
public function history(Request $request): JsonResponse public function history(Request $request): JsonResponse
{ {
$limit = $request->get('limit', 30); $limit = min(max(1, $request->integer('limit', 30)), 365);
return response()->json(AssetPrice::history($this->user->asset_id, $limit)); return response()->json(AssetPrice::history($this->user->asset_id, $limit));
} }
public function forDate(Request $request, string $date): JsonResponse public function forDate(Request $request, string $date): JsonResponse
{ {
validator(['date' => $date], ['date' => 'required|date_format:Y-m-d'])->validate();
return response()->json([ return response()->json([
'date' => $date, 'date' => $date,
'price' => AssetPrice::forDate($date, $this->user->asset_id), 'price' => AssetPrice::forDate($date, $this->user->asset_id),

View file

@ -44,7 +44,7 @@ public function share(Request $request): array
'name' => config('app.name'), 'name' => config('app.name'),
'quote' => ['message' => trim($message), 'author' => trim($author)], 'quote' => ['message' => trim($message), 'author' => trim($author)],
'auth' => [ 'auth' => [
'user' => $request->user(), 'user' => $request->user()?->only(['id', 'name', 'email']),
], ],
'ziggy' => fn (): array => [ 'ziggy' => fn (): array => [
...(new Ziggy)->toArray(), ...(new Ziggy)->toArray(),

View file

@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
/** /**
* @property int|null $asset_id * @property int|null $asset_id
@ -26,7 +27,6 @@ class User extends Authenticatable
protected $fillable = [ protected $fillable = [
'name', 'name',
'email', 'email',
'password',
'asset_id', 'asset_id',
'price_tracking_enabled', 'price_tracking_enabled',
]; ];
@ -57,10 +57,12 @@ public function asset(): BelongsTo
public static function default(): self public static function default(): self
{ {
return self::firstOrCreate( return self::firstWhere('email', 'user@incr.local')
['email' => 'user@incr.local'], ?? self::forceCreate([
['name' => 'Default User', 'password' => 'password'] 'email' => 'user@incr.local',
); 'name' => 'Default User',
'password' => bcrypt(Str::random(32)),
]);
} }
public function hasCompletedOnboarding(): bool public function hasCompletedOnboarding(): bool

View file

@ -4,6 +4,12 @@ server {
root /var/www/html/public; root /var/www/html/public;
index index.php index.html; index index.php index.html;
server_tokens off;
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "SAMEORIGIN";
add_header Referrer-Policy "strict-origin-when-cross-origin";
location / { location / {
try_files $uri $uri/ /index.php?$query_string; try_files $uri $uri/ /index.php?$query_string;
} }

View file

@ -10,7 +10,7 @@ fi
# Wait for database to be ready # Wait for database to be ready
echo "Waiting for database..." echo "Waiting for database..."
until php artisan tinker --execute="DB::connection()->getPdo();" 2>/dev/null; do until mysql -h"${DB_HOST:-db}" -u"${DB_USERNAME:-incr_user}" -p"${DB_PASSWORD}" -e "SELECT 1" >/dev/null 2>&1; do
echo "Database not ready, waiting..." echo "Database not ready, waiting..."
sleep 2 sleep 2
done done

View file

@ -1,6 +1,6 @@
[supervisord] [supervisord]
nodaemon=true nodaemon=true
user=root user=www-data
[program:nginx] [program:nginx]
command=nginx -g "daemon off;" command=nginx -g "daemon off;"