33 KiB
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 SQLsrc/app/Http/Controllers/RainfallController.php:39-90— Entire query built via string concatenation with user-supplied dates and filterssrc/app/Http/Controllers/RainfallController.php:102-191—rainfallSum()method builds SQL with string concatenation for$stationFilterand$dateFiltersrc/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 variablessrc/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 theapp/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-53—getCurrentData()src/app/Http/Controllers/Api/StationController.php:13-43—getCurrentData()src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:20-48—create()(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 SQLsrc/app/Http/Controllers/RainfallController.php:210-222— Alternative query for rainfall graph datasrc/app/Http/Controllers/RainfallController.php:285-287— Commented station lookupsrc/app/Providers/AppServiceProvider.php:23—URL::forceScheme('https')commented outsrc/app/Http/Controllers/Api/AlertController.php:23-28— Alternative topic logic commented outsrc/resources/views/layout/homeapp.blade.php:49-67— Auth section entirely commented outsrc/resources/views/layout/home.blade.php:9-10— Commented Google Maps iframesrc/resources/views/layout/admin/usermgmt.blade.php:62,75,254-256,276-278— Multiple commented out columns and pagination itemssrc/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 beCctvController.php(PascalCase)src/database/migrations/2025_11_06_072709_create_rainfall__table.php— Double underscore in filenamesrc/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 inputsrc/app/Http/Controllers/RainfallController.php:46—CAST('$displayDate' AS timestamp)— date from user input interpolatedsrc/app/Http/Controllers/RainfallController.php:69,75-81—CAST('$dateFilter' as date)— user-supplied date, multiple timessrc/app/Http/Controllers/RainfallController.php:134-136—$bindingsapproach used inrainfallSum()is better but still has one raw$sqlDateon line 134-136src/app/Http/Controllers/WaterLevelController.php:28-38— String interpolation of$stationFilterand$sqlDatewith 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:30—WHERE s.stationid = '{$stationFilter}'RainfallController.php:36—AND s.stationid = '{$stationFilter}'RainfallController.php:69—CAST('$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-51—send()method processes arbitrary data without authsrc/routes/api.php:18—Route::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 systemssrc/routes/api.php:17—Route::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')setsis_blocked = 0, absence setsis_blocked = 1src/resources/views/layout/admin/usermgmt.blade.php:131— Checkbox field namedis_blockedwith misleading togglesrc/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 onstationidortimestampsrc/database/migrations/2025_11_06_072738_create_waterlevel_table.php— No index onstationidordatetimesrc/database/migrations/2025_11_07_024940_create_notification_table.php— No index onstationid,timestamp, orstationtypesrc/database/migrations/2025_11_07_031601_create_siren__table.php— No index onstationidoractive_time
Cause: All migrations use plain column definitions without ->index().
Improvement path: Add indexes on all foreign key and filter columns:
rainfall.stationid,rainfall.timestampwaterlevel.stationid,waterlevel.datetimenotification.stationid,notification.timestamp,notification.stationtypesiren.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 subqueriessrc/app/Http/Controllers/Api/StationController.php:15-25— Same pattern duplicatedsrc/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-102—exportHistoryRfPDF()— loads ALL notification historysrc/app/Http/Controllers/NotificationController.php:108-119—exportHistoryWlPDF()— loads ALL WL historysrc/app/Http/Controllers/SirenController.php:51-58—exportHistorySirenPDF()— loads ALL siren historysrc/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-348—latest_per_hourCTE withDISTINCT ONsrc/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-86—wlHistory()doesn't validatestationidorstartDateare present before callingCarbon::parse($startDate)— would cause an error if called without parameterssrc/app/Http/Controllers/WaterLevelController.php:127-128—exportHistoricalWl()same issuesrc/app/Http/Controllers/RainfallController.php:295-299—historicalRainfall()doesn't validatestartDateorendDatebeforeCarbon::parse(); will crash withnullsrc/app/Http/Controllers/RainfallController.php:361-364—exportHourlyRainfallExcel()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:122—return redirect()->back()->with('error', $e->getMessage())src/app/Http/Controllers/AdminController.php:214— same patternsrc/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— Updatesis_blockedandlogin_attemptsthen separately updatesname,email,access_level— two separate update queries, not transactionalsrc/app/Http/Controllers/AdminController.php:72-85—storeStation()insert statement not wrapped in transactionsrc/app/Http/Controllers/AdminController.php:102-111—storeUser()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 recordssrc/app/Http/Controllers/NotificationController.php:108-113— Water level history PDF loads all recordssrc/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:
- Add composite indexes on
(stationid, timestamp)for rainfall, waterlevel, and notification tables - Implement data retention/archival policy (e.g., move data older than 1 year to archive tables)
- Replace correlated subqueries with window functions or join against derived tables
- 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.
No Input Sanitization for CCTV Links
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 validationsrc/resources/views/layout/cctv.blade.php:31—<a href="http://{{$row->cctv_link}}" target="_blank">— unvalidated URL with hardcodedhttp://prefixsrc/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 checksrc/tests/Unit/ExampleTest.php— Assertstrue === 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:8—external: trueforsides_net. The network must be manually created beforedocker compose up.- Fix: Either remove
external: trueand let Docker Compose create the network, or document the requireddocker network create sides_netstep.
Double COPY without consistent ownership:
Dockerfile:76—COPY ./src /var/www/html(copies as root)Dockerfile:79—COPY --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
COPYline. Keep only the--chown=www:wwwversion.
PostgreSQL data directory mismatch:
docker-compose.yml:29— Volume mapped to/var/lib/postgres/datainstead 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/dataor add an explicitPGDATAenvironment variable.
No queue worker process:
- The Dockerfile builds a PHP-FPM container but doesn't start a Laravel queue worker. The
QUEUE_CONNECTIONdefaults todatabase, 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:workin 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-95—DOZZLE_ENABLE_ACTIONS=trueandDOZZLE_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:103—user: "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
healthcheckdirective defined forapp,postgres, orwebservices. - 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