Files
sides/.planning/codebase/CONCERNS.md
2026-05-28 16:25:22 +08:00

33 KiB
Raw Blame History

Codebase Concerns

Analysis Date: 2026-05-28

Tech Debt

Widespread Raw SQL with String Interpolation Instead of Query Builder

Issue: The codebase predominantly uses DB::select() with raw SQL strings instead of Laravel's query builder or Eloquent. Many queries build conditions via string concatenation ('{$userInput}'), creating both SQL injection vulnerabilities and poor maintainability.

Files:

  • src/app/Http/Controllers/RainfallController.php:36-37$stationCondition = " AND s.stationid = '{$stationFilter}'" directly interpolated into raw SQL
  • src/app/Http/Controllers/RainfallController.php:39-90 — Entire query built via string concatenation with user-supplied dates and filters
  • src/app/Http/Controllers/RainfallController.php:102-191rainfallSum() method builds SQL with string concatenation for $stationFilter and $dateFilter
  • src/app/Http/Controllers/WaterLevelController.php:30,37-38 — String interpolation: $stationCondition = " WHERE s.stationid = '{$stationFilter}' " and $dateCondition = " AND w.datetime = '{$sqlDate}' "
  • src/app/Http/Controllers/WaterLevelController.php:49-57 — Raw SQL with interpolated variables
  • src/app/Http/Controllers/NotificationController.php:17-24,32-39,47-61 — Raw SQL with date literals

Impact: SQL injection risk (see Security section), code that is hard to refactor/test, and PostgreSQL-specific syntax that prevents database portability.

Fix approach: Replace all raw DB::select("SELECT ... WHERE col = '$input'") patterns with the query builder (e.g., DB::table(...)->where('col', $input)->...) or parameterized queries (DB::select("... WHERE col = ?", [$input])).

Missing Eloquent Models for Core Tables

Issue: Only src/app/Models/User.php exists as an Eloquent model. The tables station, rainfall, waterlevel, siren, and notification are queried exclusively via DB::table() or DB::select() raw SQL. There are no relationships defined, no accessors/mutators, and no model-level concerns like casting or scopes.

Files:

  • src/app/Models/User.php — Only model in the app/Models/ directory
  • All controllers use DB::table('station'), DB::table('rainfall'), etc.

Impact: Business logic is scattered across controllers. Adding columns, changing types, or adding accessors requires touching all query locations. No ability to use Eloquent relationships, eager loading, or model events.

Fix approach: Create models (Station, Rainfall, WaterLevel, Siren, Notification) with proper relationships, casts, and scopes. Migrate raw queries to Eloquent/query builder progressively.

Duplicated Dashboard Query Across Three Controllers

Issue: The same complex query (stations left-joined with latest rainfall, waterlevel, siren readings via correlated subqueries) is duplicated verbatim in three separate controllers:

Files:

  • src/app/Http/Controllers/MapController.php:25-53getCurrentData()
  • src/app/Http/Controllers/Api/StationController.php:13-43getCurrentData()
  • src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:20-48create() (login page)

Impact: Any schema change to the query requires three coordinated updates. Performance tuning must be done in three places. Increases maintenance burden.

Fix approach: Extract this query into a dedicated service class or repository method (e.g., StationService::getLatestReadings()) that can be called from all three controllers.

Large Commented-Out Code Blocks

Issue: Multiple files contain large commented-out code blocks that serve no purpose:

Files:

  • src/app/Http/Controllers/cctvController.php:13-26 — Entire block of commented-out SQL
  • src/app/Http/Controllers/RainfallController.php:210-222 — Alternative query for rainfall graph data
  • src/app/Http/Controllers/RainfallController.php:285-287 — Commented station lookup
  • src/app/Providers/AppServiceProvider.php:23URL::forceScheme('https') commented out
  • src/app/Http/Controllers/Api/AlertController.php:23-28 — Alternative topic logic commented out
  • src/resources/views/layout/homeapp.blade.php:49-67 — Auth section entirely commented out
  • src/resources/views/layout/home.blade.php:9-10 — Commented Google Maps iframe
  • src/resources/views/layout/admin/usermgmt.blade.php:62,75,254-256,276-278 — Multiple commented out columns and pagination items
  • src/resources/views/layout/threshold.blade.php:153-158,178 — Commented out graph link and hardcoded example row

Impact: Codebase bloat. Confusing for new developers. Suggests incomplete refactoring.

Fix approach: Remove all dead code. Use version control history for reference.

Inconsistent File and Class Naming

Issue: Several naming conventions violate PSR-4 and Laravel conventions:

Files:

  • src/app/Http/Controllers/cctvController.php — Should be CctvController.php (PascalCase)
  • src/database/migrations/2025_11_06_072709_create_rainfall__table.php — Double underscore in filename
  • src/database/migrations/2025_11_07_031601_create_siren__table.php — Double underscore in filename

Impact: Minor inconsistency, but violates PSR standards. Can cause issues on case-sensitive filesystems and with some tooling.

Placeholder/Lorem Ipsum Content

Issue: The landing page (home.blade.php) contains Lorem Ipsum placeholder text and generic "Card title" content:

Files:

  • src/resources/views/layout/home.blade.php:45-67 — Lorem Ipsum text and placeholder card titles

Impact: Unprofessional appearance if this page is publicly visible.

Fix approach: Replace with actual content about the SIDES system.

Security Considerations

SQL Injection in RainfallController

Risk: User-supplied $stationFilter and $dateFilter parameters are directly interpolated into raw SQL strings without parameterization.

Files:

  • src/app/Http/Controllers/RainfallController.php:34-37$stationCondition = " AND s.stationid = '{$stationFilter}'" — raw interpolation of user input
  • src/app/Http/Controllers/RainfallController.php:46CAST('$displayDate' AS timestamp) — date from user input interpolated
  • src/app/Http/Controllers/RainfallController.php:69,75-81CAST('$dateFilter' as date) — user-supplied date, multiple times
  • src/app/Http/Controllers/RainfallController.php:134-136$bindings approach used in rainfallSum() is better but still has one raw $sqlDate on line 134-136
  • src/app/Http/Controllers/WaterLevelController.php:28-38 — String interpolation of $stationFilter and $sqlDate with no parameterization

Current mitigation: None for string-interpolated queries. The rainfallSum() method in RainfallController parametrizes via bindings (partially correct), but index() and WaterLevelController::index() do not.

Recommendations: Immediately convert all string interpolation patterns to parameterized queries using either the query builder or DB::select($sql, $bindings). The most critical vulnerabilities are:

  • WaterLevelController.php:30WHERE s.stationid = '{$stationFilter}'
  • RainfallController.php:36AND s.stationid = '{$stationFilter}'
  • RainfallController.php:69CAST('$dateFilter' as date)

Unauthenticated API Endpoint Triggers Firebase Notifications

Risk: The /api/alert POST endpoint (src/app/Http/Controllers/Api/AlertController.php) accepts any request body with stationid, level, and stationtype fields and sends Firebase Cloud Messaging notifications. There is no authentication, no rate limiting, and no validation on this endpoint.

Files:

  • src/app/Http/Controllers/Api/AlertController.php:17-51send() method processes arbitrary data without auth
  • src/routes/api.php:18Route::post('/alert', [AlertController::class, 'send']) — no middleware

Current mitigation: None.

Recommendations: Add API token authentication (e.g., Laravel Sanctum), input validation, rate limiting, and logging to this endpoint. An attacker could spam all subscribers with false flood alerts.

API Endpoints Expose All Data Without Authentication

Risk: All /api/station/* endpoints (src/routes/api.php:10-17) are publicly accessible with no authentication. These expose detailed station data including IDs, names, coordinates, sensor readings, and alert levels.

Files:

  • src/app/Http/Controllers/Api/StationController.php — All methods (getCurrentData, getRainfallData, getWlData, getCurrentNoti, getHistory, getSiren, getSirenHistory)
  • src/routes/api.php:10-16 — No middleware applied

Current mitigation: None. Maps already show station locations publicly on the dashboard.

Recommendations: At minimum, apply rate limiting. Consider API authentication if these endpoints should be restricted to authorized clients only.

API Login Returns User Details Without Token

Risk: The /api/login endpoint (src/app/Http/Controllers/Api/AuthController.php) returns user id, name, email, and acc_lvl in plain JSON without issuing any session token or API key. There is no way to maintain session state in subsequent API calls.

Files:

  • src/app/Http/Controllers/Api/AuthController.php:12-56 — Custom auth that doesn't use Laravel's session or token auth systems
  • src/routes/api.php:17Route::post('/login', [AuthController::class, 'login'])

Current mitigation: None.

Recommendations: Integrate Laravel Sanctum for token-based API authentication. After login, issue a token that must be sent as a Bearer token on subsequent requests.

XSS via Unescaped Blade Output in Station Management

Risk: The station management view uses {!! !!} (unescaped Blade output) to render station type badges. These badge values are built from user-controlled data stored in the database ($row->rainfall, $row->waterlevel, $row->siren).

Files:

  • src/resources/views/layout/admin/stationmgmt.blade.php:110{!! $types ? implode(' ', $types) : '<span class="badge bg-secondary">No Type</span>' !!}

Current mitigation: The badge values (rainfall, waterlevel, siren) are integers (0 or 1), so the risk is limited to the label text. However, other display data like $row->name, $row->district, etc. comes from user input (stored via AdminController::storeStation()). These are output elsewhere via {{ }} (escaped), but this one {!! !!} instance opens a vector if the underlying data is ever manipulated.

Recommendations: Replace {!! $types ... !!} with {{ $types ... }} and build the HTML span tags client-side or via safe Blade components. Always escape dynamic content.

Password Policy Inconsistency

Risk: User passwords set via the admin panel (AdminController::storeUser, updatePassword) only require min:6 characters, while user self-registration uses Rules\Password::defaults() (which enforces more stringent rules like mixed case, special characters, etc.).

Files:

  • src/app/Http/Controllers/AdminController.php:98'password' => 'required|string|min:6|confirmed'
  • src/app/Http/Controllers/AdminController.php:229'password' => 'required|string|min:6|confirmed'
  • src/app/Http/Controllers/Auth/RegisteredUserController.php:35'password' => ['required', 'confirmed', Rules\Password::defaults()]

Current mitigation: Both paths use bcrypt()/Hash::make() so passwords are hashed.

Recommendations: Standardize on Rules\Password::defaults() for all password creation paths.

Inverted Block/Unblock Logic in User Management

Risk: The updateUsers() method in AdminController has inverted logic for the is_blocked checkbox. When the checkbox is present (checked), the user is set to is_blocked = 0 (unblocked). When absent (unchecked), the user is set to is_blocked = 1 (blocked). This creates a confusing UX where checking the "enable account" box actually means "unblock the user."

Files:

  • src/app/Http/Controllers/AdminController.php:177-192$request->has('is_blocked') sets is_blocked = 0, absence sets is_blocked = 1
  • src/resources/views/layout/admin/usermgmt.blade.php:131 — Checkbox field named is_blocked with misleading toggle
  • src/resources/views/layout/admin/usermgmt.blade.php:133 — Label reads __('messages.block') when unchecked, __('messages.active') when checked (inverted from what the name implies)

Recommendations: Rename the checkbox to something semantically clear like unblock_account. Fix the logic so checked = blocked status = true, unchecked = blocked = false. Or restructure to use a dedicated block and unblock endpoint.

No CSRF Protection on Web Auth Routes (post-login)

Risk: While Laravel applies CSRF protection to web routes by default, the API routes have no CSRF protection, which is expected. However, the AuthenticatedSessionController::store() has custom authentication logic that bypasses the standard LoginRequest::authenticate() method. The controller first queries for the user manually on line 62 before calling Auth::attempt().

Files:

  • src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:55-97 — Custom login logic diverges from Breeze scaffolding

Current mitigation: Standard CSRF token is still present on the login form view (src/resources/views/layout/app.blade.php:31 includes @csrf).

Recommendations: Keep CSRF protection on all web routes. The custom authentication logic should be reviewed for race conditions (user looks up is_blocked, then Auth::attempt runs).

FCM Credentials Exceptions

Risk: FcmService::__construct() calls file_get_contents() on a path derived from env('FIREBASE_CREDENTIALS'). If the file doesn't exist or the path is misconfigured, this throws an uncaught \Exception that returns a 500 error.

Files:

  • src/app/Services/FcmService.php:17-20 — No error handling around file access

Current mitigation: None.

Recommendations: Wrap the file read in a try-catch, validate the file exists first, and provide a meaningful error response.

Performance Bottlenecks

Missing Database Indexes on Joined Columns

Problem: Several tables lack indexes on columns used extensively in JOINs and WHERE filters.

Files:

  • src/database/migrations/2025_11_06_072709_create_rainfall__table.php — No index on stationid or timestamp
  • src/database/migrations/2025_11_06_072738_create_waterlevel_table.php — No index on stationid or datetime
  • src/database/migrations/2025_11_07_024940_create_notification_table.php — No index on stationid, timestamp, or stationtype
  • src/database/migrations/2025_11_07_031601_create_siren__table.php — No index on stationid or active_time

Cause: All migrations use plain column definitions without ->index().

Improvement path: Add indexes on all foreign key and filter columns:

  • rainfall.stationid, rainfall.timestamp
  • waterlevel.stationid, waterlevel.datetime
  • notification.stationid, notification.timestamp, notification.stationtype
  • siren.stationid, siren.active_time

Inefficient Correlated Subqueries in Dashboard Queries

Problem: The repeated dashboard query uses correlated subqueries (e.g., SELECT MAX(timestamp) FROM rainfall WHERE stationid = s.stationid) that execute once per station row in the outer query. For 50 stations, this executes 150+ subqueries (rainfall + waterlevel + siren × stations).

Files:

  • src/app/Http/Controllers/MapController.php:27-37 — Three correlated subqueries
  • src/app/Http/Controllers/Api/StationController.php:15-25 — Same pattern duplicated
  • src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:22-32 — Same pattern tripled

Cause: PostgreSQL can optimize these with appropriate indexes, but without them, this is a full table scan × stations count.

Improvement path: Add indexes as above. Consider using window functions (ROW_NUMBER() OVER (PARTITION BY stationid ORDER BY timestamp DESC)) instead of correlated subqueries for potentially better performance with large datasets.

Unpaginated Large Exports and API Endpoints

Problem: Multiple export methods and API endpoints load ALL matching records at once without pagination:

Files:

  • src/app/Http/Controllers/NotificationController.php:93-102exportHistoryRfPDF() — loads ALL notification history
  • src/app/Http/Controllers/NotificationController.php:108-119exportHistoryWlPDF() — loads ALL WL history
  • src/app/Http/Controllers/SirenController.php:51-58exportHistorySirenPDF() — loads ALL siren history
  • src/app/Http/Controllers/Api/StationController.php:43,64,86,106,129,147,161 — All API endpoints load unfiltered/complete datasets

Cause: No pagination or date-range limits implemented.

Improvement path: Add date range restrictions and/or chunked processing for exports. Add pagination (->paginate() or ->limit()) to API endpoints. For PDFs, consider streaming or limiting to a maximum record count.

Complex Window Functions Without Indexes

Problem: The historicalRainfall() method and HourlyRainfallExport use DISTINCT ON with window functions partitioned by stationid and EXTRACT(HOUR FROM timestamp). Without proper indexes, this forces sequential scans.

Files:

  • src/app/Http/Controllers/RainfallController.php:322-348latest_per_hour CTE with DISTINCT ON
  • src/app/Exports/HourlyRainfallExport.php:35-54 — Same pattern duplicated for Excel export

Improvement path: Add composite index on rainfall(stationid, timestamp) to support the DISTINCT ON ordering.

Reliability Concerns

Missing Validation on Required Parameters

Issue: Several controller methods fail to validate that required parameters are provided before using them:

Files:

  • src/app/Http/Controllers/WaterLevelController.php:84-86wlHistory() doesn't validate stationid or startDate are present before calling Carbon::parse($startDate) — would cause an error if called without parameters
  • src/app/Http/Controllers/WaterLevelController.php:127-128exportHistoricalWl() same issue
  • src/app/Http/Controllers/RainfallController.php:295-299historicalRainfall() doesn't validate startDate or endDate before Carbon::parse(); will crash with null
  • src/app/Http/Controllers/RainfallController.php:361-364exportHourlyRainfallExcel() same issue

Exception Messages Leaked to Users

Issue: Several controllers catch generic \Exception and pass $e->getMessage() directly to redirect back with error flash:

Files:

  • src/app/Http/Controllers/AdminController.php:122return redirect()->back()->with('error', $e->getMessage())
  • src/app/Http/Controllers/AdminController.php:214 — same pattern
  • src/app/Http/Controllers/AdminController.php:253 — same pattern

Impact: In production with APP_DEBUG=false, this still leaks internal exception messages to end users. These may contain SQL syntax errors, file paths, or other system information. In debug mode (APP_DEBUG=true), full stack traces would be shown.

Fix approach: Log the actual exception with Log::error(), and return a generic user-friendly message.

Missing Transaction Safety for Write Operations

Issue: Multiple sequential database write operations are not wrapped in database transactions. A failure midway can leave data in an inconsistent state:

Files:

  • src/app/Http/Controllers/AdminController.php:196-204 — Updates is_blocked and login_attempts then separately updates name, email, access_level — two separate update queries, not transactional
  • src/app/Http/Controllers/AdminController.php:72-85storeStation() insert statement not wrapped in transaction
  • src/app/Http/Controllers/AdminController.php:102-111storeUser() insert with no transaction

Fix approach: Wrap write operations in DB::transaction().

Potential Memory Exhaustion from Large PDF Generation

Issue: The DomPDF exports load all matching records into memory via ->get(), then pass the entire collection to DomPDF. For datasets with months of history records, this could exhaust PHP memory limits.

Files:

  • src/app/Http/Controllers/NotificationController.php:93-97 — Rainfall history PDF loads all records
  • src/app/Http/Controllers/NotificationController.php:108-113 — Water level history PDF loads all records
  • src/app/Http/Controllers/SirenController.php:51-58 — Siren history PDF loads all records

Fix approach: Add date range filtering (at least a 90-day cap), add pagination, or chunk the data and batch-generate PDF pages.

Fragile Areas

WaterLevelController String Interpolation Queries

Files: src/app/Http/Controllers/WaterLevelController.php:28-57

Why fragile: The entire method builds conditions via string concatenation with user input. Adding a new filter condition or changing the query logic requires careful string manipulation. Any missing space or quote in the concatenation breaks the query. Additionally, these SQL injection vectors mean a malicious input could drop tables or exfiltrate data.

Safe modification: Convert to query builder: DB::table('station as s')->join('waterlevel as w', ...)->when($stationFilter, fn($q) => $q->where('s.stationid', $stationFilter))->when($dateFilter, fn($q) => $q->where('w.datetime', $dateFilter)).

Test coverage: No tests exist for this controller (only example tests present).

AuthenticatedSessionController Custom Login Logic

Files: src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:55-97

Why fragile: The custom login flow manually queries the user, checks is_blocked, attempts auth, then manually manages login_attempts and is_blocked fields. This duplicates (and partially overrides) the standard LoginRequest::authenticate() behavior. The logic for incrementing login_attempts and blocking after 3 failures is a race condition — concurrent login attempts could each increment before the user is blocked, allowing more attempts than intended.

Safe modification: Move login attempt tracking and blocking logic into a proper authentication middleware or event listener. Use database transactions or atomic increments for login_attempts.

Test coverage: No test exists for the custom authentication flow.

FcmService Credential Loading

Files: src/app/Services/FcmService.php:15-21

Why fragile: The constructor reads FIREBASE_CREDENTIALS from env, then calls file_get_contents() on the resolved path. If env is empty, the file is missing, or the JSON is malformed, this throws exceptions that propagate as 500 errors. The credentials are read on every request (service is instantiated per-request), causing unnecessary filesystem I/O.

Safe modification: Cache the credentials in memory (singleton service), validate the file exists before reading, and throw a specific exception with proper logging.

Scaling Limits

Database Table Growth for Sensor Readings

Current capacity: No limits defined. The rainfall, waterlevel, and notification tables collect time-series sensor data that grows indefinitely.

Limit: Without indexes and with correlated subqueries, performance degrades as these tables grow. The dashboard query scans the max timestamp for each station — with 50+ stations and years of data, this becomes slow.

Scaling path:

  1. Add composite indexes on (stationid, timestamp) for rainfall, waterlevel, and notification tables
  2. Implement data retention/archival policy (e.g., move data older than 1 year to archive tables)
  3. Replace correlated subqueries with window functions or join against derived tables
  4. Consider partitioning rainfall and waterlevel tables by month/year

PDF and Excel Exports for Large Datasets

Current capacity: All exports load complete result sets into memory.

Limit: With tens of thousands of notification records, DomPDF and Laravel-Excel will exhaust PHP memory (typically 128MB-256MB container memory).

Scaling path: Add maximum date range limits (e.g., 90 days), add record count limits, or implement chunked/streamed export.

Dependencies at Risk

barryvdh/laravel-dompdf (^3.1)

Risk: DomPDF has known memory limitations for large HTML-to-PDF conversions. It also depends on the Doctrine XML parser and can produce inconsistent rendering.

Impact: Large exports (notification history) will fail silently or produce 500 errors.

Migration plan: Consider laravel-snappy (wkhtmltopdf wrapper) for better rendering, or a SaaS solution like PDF.co for high-volume exports. For now, add date range limits and memory limit increases.

google/auth (^1.49)

Risk: The google/auth library is used directly (not via Laravel's Firebase package like kreait/laravel-firebase) in FcmService.php. This is a lower-level implementation that requires manual token management.

Impact: Relies on manual credential loading and token fetching. If Google's auth endpoints change, the implementation breaks. No config caching or token reuse.

Migration plan: Either switch to kreait/laravel-firebase (official Firebase PHP SDK for Laravel) or add token caching to the current implementation to reduce auth requests.

maatwebsite/excel (^3.1)

Risk: Version 3.1 is stable but receives less frequent updates. The package wraps PhpSpreadsheet, which uses significant memory for large exports.

Impact: Large historical rainfall exports could exhaust memory.

Migration plan: For Excel exports, limit result sets to reasonable sizes. Consider streaming writes for large datasets.

Missing Critical Features

No API Rate Limiting

Problem: None of the API routes apply rate limiting. An attacker can hit any endpoint arbitrarily, including the unauthenticated /api/alert endpoint.

Blocks: Prevents abuse detection and mitigation.

No API Authentication for Mobile/Third-Party Clients

Problem: The API has a /api/login endpoint that returns user info but no token. There's no mechanism to authenticate subsequent API requests. All data endpoints are effectively public.

Blocks: Restricting access to authorized mobile app users.

No Audit Logging

Problem: There is no logging of admin actions — station creation, user creation, password changes, or data exports are not logged.

Files: All AdminController methods lack logging.

Blocks: Auditing who changed what and when for security compliance.

No Health Check Endpoint

Problem: The Docker containers have no health checks, and the application has no health check endpoint.

Blocks: Orchestration tools (Docker, K8s) cannot verify application health.

Problem: CCTV links (cctv_link) stored in the database are output directly as href="http://{{$row->cctv_link}}" in the CCTV view with a hardcoded http:// prefix. Users can input any URL including javascript: URLs or phishing links.

Files:

  • src/app/Http/Controllers/AdminController.php:65'cctv_link' => 'nullable|string|max:500' — no URL validation
  • src/resources/views/layout/cctv.blade.php:31<a href="http://{{$row->cctv_link}}" target="_blank"> — unvalidated URL with hardcoded http:// prefix
  • src/resources/views/layout/admin/stationmgmt.blade.php:183 — CCTV link input accepts arbitrary text

Impact: An admin could paste a malicious or malformed URL. The hardcoded http:// prefix means legitimate HTTPS links would be broken (e.g., https://example.com becomes http://https://example.com).

Test Coverage Gaps

Near-Zero Test Coverage

What's not tested:

  • All API controllers (StationController, AuthController, AlertController) — no tests exist
  • All web controllers (RainfallController, WaterLevelController, SirenController, NotificationController, AdminController, MapController, cctvController, LocaleController) — no tests exist
  • FcmService — no tests exist
  • All export classes (HourlyRainfallExport, WaterLevelExport) — no tests exist
  • The custom authentication flow in AuthenticatedSessionController — no tests exist
  • The AdminMiddleware — no tests exist
  • The LocalizationMiddleware — no tests exist

Files: Only two example tests exist:

  • src/tests/Feature/ExampleTest.php — Basic HTTP 200 check
  • src/tests/Unit/ExampleTest.php — Asserts true === true

Risk: Any refactoring, especially of the SQL-heavy controllers, could introduce regressions with no safety net. The authentication system (block logic, login_attempts) is particularly risky.

Priority: High

Configuration Concerns

Missing .env.example

Issue: The composer setup script attempts to copy .env.example to .env, but no .env.example file exists in src/. This means the setup step fails silently and the application may run with default config values (unsafe defaults like empty database passwords).

Files:

  • src/composer.json:41"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

Impact: Developers/ops cannot easily replicate the environment configuration.

Default Session Encryption Disabled

Issue: config/session.php defaults 'encrypt' => env('SESSION_ENCRYPT', false). Session data is not encrypted by default.

Impact: If sessions are stored in the database (the default driver), session data is readable by anyone with database access.

Default DB Password is Empty String

Issue: Multiple database connection configs default password to an empty string:

  • config/database.php:53 — MySQL: 'password' => env('DB_PASSWORD', '')
  • config/database.php:73 — MariaDB: 'password' => env('DB_PASSWORD', '')
  • config/database.php:93 — PostgreSQL: 'password' => env('DB_PASSWORD', '')
  • config/database.php:108 — SQLSRV: 'password' => env('DB_PASSWORD', '')

Impact: If the .env file is missing the DB_PASSWORD key, the database will be accessed with an empty password.

Deployment Concerns

Docker Setup Issues

External network requirement:

  • docker-compose.yml:8external: true for sides_net. The network must be manually created before docker compose up.
  • Fix: Either remove external: true and let Docker Compose create the network, or document the required docker network create sides_net step.

Double COPY without consistent ownership:

  • Dockerfile:76COPY ./src /var/www/html (copies as root)
  • Dockerfile:79COPY --chown=www:www ./src /var/www/html (copies again as www)
  • Impact: Files are copied twice, first as root then as www. The second copy should be the only one needed.
  • Fix: Remove the first COPY line. Keep only the --chown=www:www version.

PostgreSQL data directory mismatch:

  • docker-compose.yml:29 — Volume mapped to /var/lib/postgres/data instead of default /var/lib/postgresql/data
  • Impact: If PostgreSQL expects data in the default location, startup may fail or data won't persist.
  • Fix: Use the standard PostgreSQL data directory /var/lib/postgresql/data or add an explicit PGDATA environment variable.

No queue worker process:

  • The Dockerfile builds a PHP-FPM container but doesn't start a Laravel queue worker. The QUEUE_CONNECTION defaults to database, but there's no process actually processing jobs.
  • Impact: Password reset emails and any queued notifications will never be sent.
  • Fix: Add a supervisor configuration to run php artisan queue:work in the container.

pgAdmin and Adminer exposed in production config:

  • docker-compose.yml:54-67,70-79 — pgAdmin (port 5050) and Adminer (port 6060) are configured alongside the main services. If deployed to production, these are database management interfaces exposed on the network.
  • Impact: Increased attack surface. Compromise of pgAdmin/Adminer gives direct database access.
  • Fix: Separate infrastructure tools into a development-only docker-compose.override.yml file.

Dozzle with shell and container actions enabled:

  • docker-compose.yml:89-95DOZZLE_ENABLE_ACTIONS=true and DOZZLE_ENABLE_SHELL=true — allows anyone with access to Dozzle to execute commands in any container.
  • Impact: Significant security risk in production environments.
  • Fix: Disable shell and actions in production, or add authentication.

Filebrowser runs as root:

  • docker-compose.yml:103user: "0:0" — FileBrowser container runs as root with access to /root/sides.
  • Impact: If FileBrowser is compromised, the attacker has root access to the mounted directories.

No health checks on any service:

  • No healthcheck directive defined for app, postgres, or web services.
  • Impact: Docker has no way to know if services are healthy. Dependencies (depends_on) only wait for container start, not service readiness.

Nginx Security Headers Gaps

Issue: The Nginx config sets X-Frame-Options, X-XSS-Protection, and X-Content-Type-Options but is missing:

  • Strict-Transport-Security (HSTS)
  • Content-Security-Policy (CSP)
  • Referrer-Policy

Files:

  • docker/nginx/default.conf:8-10 — Only three security headers set

Impact: The application is more susceptible to clickjacking (partially mitigated), MIME-type sniffing (mitigated), and lacks comprehensive CSP that could prevent XSS.


Concerns audit: 2026-05-28