refactor(cli): validate --date and escape shell args on logs:scheduled
Reject malformed --date values with a clear error before building any shell command, and wrap every interpolated value (log paths, filter expression, line count) in escapeshellarg() so filter options and date values can no longer break out of the tail/grep pipeline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
49b5472961
commit
bb0c3501ef
2 changed files with 55 additions and 10 deletions
|
|
@ -28,6 +28,11 @@ class ViewScheduledLogs extends Command
|
|||
public function handle()
|
||||
{
|
||||
$date = $this->option('date') ?: now()->format('Y-m-d');
|
||||
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
$this->error('Invalid date format. Use Y-m-d (e.g. 2025-01-31).');
|
||||
|
||||
return self::INVALID;
|
||||
}
|
||||
$logPaths = $this->getLogPaths($date);
|
||||
|
||||
if (empty($logPaths)) {
|
||||
|
|
@ -49,17 +54,19 @@ public function handle()
|
|||
$this->line('');
|
||||
|
||||
if (count($logPaths) === 1) {
|
||||
$logPath = $logPaths[0];
|
||||
$logPath = escapeshellarg($logPaths[0]);
|
||||
if ($filters) {
|
||||
passthru("tail -f {$logPath} | grep -E '{$filters}'");
|
||||
$escapedFilters = escapeshellarg($filters);
|
||||
passthru("tail -f {$logPath} | grep -E {$escapedFilters}");
|
||||
} else {
|
||||
passthru("tail -f {$logPath}");
|
||||
}
|
||||
} else {
|
||||
// Multiple files - use multitail or tail with process substitution
|
||||
$logPathsStr = implode(' ', $logPaths);
|
||||
$logPathsStr = implode(' ', array_map('escapeshellarg', $logPaths));
|
||||
if ($filters) {
|
||||
passthru("tail -f {$logPathsStr} | grep -E '{$filters}'");
|
||||
$escapedFilters = escapeshellarg($filters);
|
||||
passthru("tail -f {$logPathsStr} | grep -E {$escapedFilters}");
|
||||
} else {
|
||||
passthru("tail -f {$logPathsStr}");
|
||||
}
|
||||
|
|
@ -68,20 +75,23 @@ public function handle()
|
|||
$this->info("Showing last {$lines} lines of {$logTypeDescription} logs for {$date}{$filterDescription}:");
|
||||
$this->line('');
|
||||
|
||||
$escapedLines = escapeshellarg((string) $lines);
|
||||
if (count($logPaths) === 1) {
|
||||
$logPath = $logPaths[0];
|
||||
$logPath = escapeshellarg($logPaths[0]);
|
||||
if ($filters) {
|
||||
passthru("tail -n {$lines} {$logPath} | grep -E '{$filters}'");
|
||||
$escapedFilters = escapeshellarg($filters);
|
||||
passthru("tail -n {$escapedLines} {$logPath} | grep -E {$escapedFilters}");
|
||||
} else {
|
||||
passthru("tail -n {$lines} {$logPath}");
|
||||
passthru("tail -n {$escapedLines} {$logPath}");
|
||||
}
|
||||
} else {
|
||||
// Multiple files - concatenate and sort by timestamp
|
||||
$logPathsStr = implode(' ', $logPaths);
|
||||
$logPathsStr = implode(' ', array_map('escapeshellarg', $logPaths));
|
||||
if ($filters) {
|
||||
passthru("tail -n {$lines} {$logPathsStr} | sort | grep -E '{$filters}'");
|
||||
$escapedFilters = escapeshellarg($filters);
|
||||
passthru("tail -n {$escapedLines} {$logPathsStr} | sort | grep -E {$escapedFilters}");
|
||||
} else {
|
||||
passthru("tail -n {$lines} {$logPathsStr} | sort");
|
||||
passthru("tail -n {$escapedLines} {$logPathsStr} | sort");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
tests/Feature/ScheduledLogsCommandInputTest.php
Normal file
35
tests/Feature/ScheduledLogsCommandInputTest.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use App\Console\Commands\ViewScheduledLogs;
|
||||
use App\Http\Middleware\CheckForcePasswordReset;
|
||||
use App\Http\Middleware\DecideWhatToDoWithUser;
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Once;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->withoutMiddleware([DecideWhatToDoWithUser::class, CheckForcePasswordReset::class]);
|
||||
Once::flush();
|
||||
if (! InstanceSettings::find(0)) {
|
||||
$settings = new InstanceSettings;
|
||||
$settings->id = 0;
|
||||
$settings->saveQuietly();
|
||||
}
|
||||
});
|
||||
|
||||
describe('logs:scheduled --date option', function () {
|
||||
test('rejects a malformed date and exits before touching the shell', function () {
|
||||
$this->artisan('logs:scheduled', ['--date' => '2025-01-01; touch /tmp/pwn'])
|
||||
->expectsOutputToContain('Invalid date format')
|
||||
->assertExitCode(ViewScheduledLogs::INVALID);
|
||||
|
||||
expect(file_exists('/tmp/pwn'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('accepts a well-formed date', function () {
|
||||
$this->artisan('logs:scheduled', ['--date' => '2025-01-01'])
|
||||
->assertExitCode(0);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue