Skip to content

Commit 46e060a

Browse files
author
jakub-przepiora
committed
security: fix critical and high vulnerabilities — v0.4.1
CRITICAL: - InstallController: add isInstalled() guard to setupEnvironment() and setupDatabase() POST handlers - CsvImportController: block path traversal — enforce csv-imports/ prefix on file_path - API login: add throttle:10,1 rate limiting - CORS: disable supports_credentials (was wildcard+credentials — invalid/risky combination) HIGH: - API routes: add role:Admin to audit-log/event-log endpoints - API routes: add role:Supervisor|Admin to analytics/reports endpoints - SettingsController: fix IDOR — verify token ownership before revocation - UpdateController: wrap exec() path in escapeshellarg() - ActionExecutor: block SSRF in webhook forwarding — reject private/loopback/metadata IPs - Web login: add throttle:10,1 rate limiting - composer: update league/commonmark to patched version (XSS bypass fix) - package.json: add overrides for rollup and picomatch (HIGH severity advisories)
1 parent 3f26b1c commit 46e060a

11 files changed

Lines changed: 78 additions & 34 deletions

File tree

backend/app/Http/Controllers/InstallController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ public function showEnvironmentForm()
8383
*/
8484
public function setupEnvironment(Request $request)
8585
{
86+
if ($this->isInstalled()) {
87+
return redirect('/');
88+
}
89+
8690
$validated = $request->validate([
8791
'app_name' => 'required|string|max:255',
8892
'app_url' => 'required|url',
@@ -135,6 +139,10 @@ public function showDatabaseForm()
135139
*/
136140
public function setupDatabase(Request $request)
137141
{
142+
if ($this->isInstalled()) {
143+
return redirect('/');
144+
}
145+
138146
$driver = $request->input('db_driver', 'pgsql');
139147

140148
if (!array_key_exists($driver, self::DB_DRIVERS)) {

backend/app/Http/Controllers/Web/Admin/CsvImportController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ public function process(Request $request)
103103
'production_year' => 'nullable|integer|min:2000|max:2100',
104104
]);
105105

106-
$filePath = $request->input('file_path');
106+
$filePath = $request->input('file_path');
107+
if (!str_starts_with($filePath, 'csv-imports/')) {
108+
abort(422, 'Invalid file path.');
109+
}
107110
$strategy = $request->input('import_strategy');
108111
$mapping = $request->input('mapping');
109112
$targetLineId = $request->input('target_line_id');

backend/app/Http/Controllers/Web/Admin/UpdateController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function apply(): RedirectResponse
6363
// 1. git pull
6464
$gitOutput = [];
6565
$gitStatus = 0;
66-
exec("cd {$root} && git pull origin main 2>&1", $gitOutput, $gitStatus);
66+
exec('cd ' . escapeshellarg($root) . ' && git pull origin main 2>&1', $gitOutput, $gitStatus);
6767
$log[] = implode("\n", $gitOutput);
6868

6969
if ($gitStatus !== 0) {
@@ -75,7 +75,7 @@ public function apply(): RedirectResponse
7575

7676
// 2. composer install
7777
$composerOutput = [];
78-
exec("cd {$root} && composer install --no-dev --optimize-autoloader 2>&1", $composerOutput);
78+
exec('cd ' . escapeshellarg($root) . ' && composer install --no-dev --optimize-autoloader 2>&1', $composerOutput);
7979
$log[] = implode("\n", $composerOutput);
8080

8181
// 3. artisan

backend/app/Http/Controllers/Web/SettingsController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ public function createApiToken(Request $request)
131131
*/
132132
public function revokeApiToken(Request $request, PersonalAccessToken $token)
133133
{
134+
abort_if(
135+
$token->tokenable_id !== auth()->id() || $token->tokenable_type !== get_class(auth()->user()),
136+
403
137+
);
138+
134139
$token->delete();
135140

136141
return redirect()->route('settings.api-tokens')

backend/app/Services/Connectivity/ActionExecutor.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,26 @@ private function webhookForward(array $params, array $data): array
246246
throw new \RuntimeException("Invalid or missing webhook URL");
247247
}
248248

249+
// Block SSRF — reject requests to private/loopback/metadata addresses
250+
$host = parse_url($url, PHP_URL_HOST);
251+
if ($host) {
252+
$ip = gethostbyname($host);
253+
if (filter_var($ip, FILTER_VALIDATE_IP)) {
254+
foreach ([
255+
FILTER_FLAG_NO_PRIV_RANGE,
256+
FILTER_FLAG_NO_RES_RANGE,
257+
] as $flag) {
258+
if (!filter_var($ip, FILTER_VALIDATE_IP, $flag)) {
259+
throw new \RuntimeException("Webhook URL resolves to a private/reserved address");
260+
}
261+
}
262+
// Block AWS/GCP/Azure metadata endpoints explicitly
263+
if (in_array($ip, ['169.254.169.254', '169.254.170.2', '100.100.100.200'])) {
264+
throw new \RuntimeException("Webhook URL resolves to a private/reserved address");
265+
}
266+
}
267+
}
268+
249269
$response = Http::withHeaders($headers)
250270
->timeout(5)
251271
->{$method}($url, $data);

backend/composer.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/config/cors.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929

3030
'max_age' => 0,
3131

32-
'supports_credentials' => true,
32+
'supports_credentials' => false,
3333

3434
];

backend/config/version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?php
22

33
return [
4-
'current' => 'v0.4.0',
4+
'current' => 'v0.4.1',
55
];

backend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@
1717
"dependencies": {
1818
"alpinejs": "^3.15.8",
1919
"chart.js": "^4.5.1"
20+
},
21+
"overrides": {
22+
"rollup": "^4.29.1",
23+
"picomatch": "^4.0.2"
2024
}
2125
}

backend/routes/api.php

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
// Authentication routes (no auth required)
3535
Route::prefix('auth')->group(function () {
36-
Route::post('/login', [AuthController::class, 'login']);
36+
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:10,1');
3737
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
3838
Route::post('/refresh', [AuthController::class, 'refresh'])->middleware('auth:sanctum');
3939
Route::post('/change-password', [AuthController::class, 'changePassword'])->middleware('auth:sanctum');
@@ -85,25 +85,29 @@
8585
Route::post('/csv-import-mappings', [CsvImportController::class, 'saveMapping']);
8686

8787
// Audit Logs (Admin only)
88-
Route::get('/audit-logs', [AuditLogController::class, 'index']);
89-
Route::get('/audit-logs/entity', [AuditLogController::class, 'entity']);
90-
Route::get('/audit-logs/export', [AuditLogController::class, 'export']);
88+
Route::middleware('role:Admin')->group(function () {
89+
Route::get('/audit-logs', [AuditLogController::class, 'index']);
90+
Route::get('/audit-logs/entity', [AuditLogController::class, 'entity']);
91+
Route::get('/audit-logs/export', [AuditLogController::class, 'export']);
9192

92-
// Event Logs
93-
Route::get('/event-logs', [EventLogController::class, 'index']);
94-
Route::get('/event-logs/entity', [EventLogController::class, 'entity']);
93+
// Event Logs
94+
Route::get('/event-logs', [EventLogController::class, 'index']);
95+
Route::get('/event-logs/entity', [EventLogController::class, 'entity']);
96+
});
9597

96-
// Analytics (Supervisor Dashboard)
97-
Route::get('/analytics/overview', [AnalyticsController::class, 'overview']);
98-
Route::get('/analytics/production-by-line', [AnalyticsController::class, 'productionByLine']);
99-
Route::get('/analytics/cycle-time', [AnalyticsController::class, 'cycleTime']);
100-
Route::get('/analytics/throughput', [AnalyticsController::class, 'throughput']);
101-
Route::get('/analytics/issue-stats', [AnalyticsController::class, 'issueStats']);
102-
Route::get('/analytics/step-performance', [AnalyticsController::class, 'stepPerformance']);
98+
// Analytics (Supervisor/Admin)
99+
Route::middleware('role:Supervisor|Admin')->group(function () {
100+
Route::get('/analytics/overview', [AnalyticsController::class, 'overview']);
101+
Route::get('/analytics/production-by-line', [AnalyticsController::class, 'productionByLine']);
102+
Route::get('/analytics/cycle-time', [AnalyticsController::class, 'cycleTime']);
103+
Route::get('/analytics/throughput', [AnalyticsController::class, 'throughput']);
104+
Route::get('/analytics/issue-stats', [AnalyticsController::class, 'issueStats']);
105+
Route::get('/analytics/step-performance', [AnalyticsController::class, 'stepPerformance']);
103106

104-
// Reports (Supervisor/Admin)
105-
Route::get('/reports/production-summary', [ReportController::class, 'productionSummary']);
106-
Route::get('/reports/batch-completion', [ReportController::class, 'batchCompletion']);
107-
Route::get('/reports/downtime', [ReportController::class, 'downtimeReport']);
108-
Route::get('/reports/export-csv', [ReportController::class, 'exportCsv']);
107+
// Reports
108+
Route::get('/reports/production-summary', [ReportController::class, 'productionSummary']);
109+
Route::get('/reports/batch-completion', [ReportController::class, 'batchCompletion']);
110+
Route::get('/reports/downtime', [ReportController::class, 'downtimeReport']);
111+
Route::get('/reports/export-csv', [ReportController::class, 'exportCsv']);
112+
});
109113
});

0 commit comments

Comments
 (0)