From dd6c9b7dc5d6f38bc431c0c937c3e37adce4d028 Mon Sep 17 00:00:00 2001 From: Crivion Date: Wed, 22 Oct 2025 10:08:21 +0300 Subject: [PATCH] firewall: fix empty spec when storing ufw rule --- app/Http/Controllers/FirewallController.php | 65 ++++++++++++++------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/app/Http/Controllers/FirewallController.php b/app/Http/Controllers/FirewallController.php index 586486f..51b046d 100644 --- a/app/Http/Controllers/FirewallController.php +++ b/app/Http/Controllers/FirewallController.php @@ -3,18 +3,17 @@ namespace App\Http\Controllers; use App\Actions\Firewall\AddUfwRuleAction; +use App\Actions\Firewall\AddUfwDenyRuleAction; use App\Actions\Firewall\DeleteUfwRuleAction; use App\Actions\Firewall\GetUfwRulesAction; use App\Actions\Firewall\GetUfwStatusAction; use App\Actions\Firewall\ToggleUfwAction; -use App\Http\Middleware\AdminMiddleware; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Inertia; class FirewallController extends Controller { - public function index(): \Inertia\Response { $status = (new GetUfwStatusAction())->execute(); @@ -38,43 +37,67 @@ class FirewallController extends Controller public function store(Request $request): RedirectResponse { $validated = $request->validate([ - 'type' => 'required|string|in:allow,deny', + 'type' => 'required|string|in:allow,deny', 'protocol' => 'required|string|in:tcp,udp', - 'port' => 'required|integer|min:1|max:65535', - 'ip' => 'required|string', // validated below for any|ip|cidr - 'to' => 'required|string', - 'comment' => 'nullable|string|max:150', + 'port' => 'required|integer|min:1|max:65535', + 'ip' => 'required|string', // validated below for any|ip|cidr + 'to' => 'required|string', + 'comment' => 'nullable|string|max:150', ]); $from = trim($validated['ip']); - $to = trim($validated['to']); + $to = trim($validated['to']); - $isAny = fn(string $v) => strtolower($v) === 'any'; - $isIp = fn(string $v) => filter_var($v, FILTER_VALIDATE_IP) !== false; - $isCidr = fn(string $v) => (bool) preg_match('/^((25[0-5]|2[0-4]\\d|1?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|1?\\d?\\d)){3})\\/(3[0-2]|[12]?\\d)$/', $v); + + $isAny = fn(string $v) => strtolower($v) === 'any'; + $isIp = fn(string $v) => filter_var($v, FILTER_VALIDATE_IP) !== false; + $isCidr = fn(string $v) => (bool) preg_match( + '/^((25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3})\/(3[0-2]|[12]?\d)$/', + $v + ); if (!($isAny($from) || $isIp($from) || $isCidr($from))) { - return back()->withErrors(['ip' => 'IP must be "any", a valid IP address, or CIDR range.'])->withInput(); - } - if (!($isAny($to) || $isIp($to))) { - return back()->withErrors(['to' => 'To must be "any" or a valid IP address.'])->withInput(); + return back() + ->withErrors(['ip' => 'IP must be "any", a valid IP address, or CIDR range.']) + ->withInput(); } - $proto = strtolower($validated['protocol']); - $port = (int) $validated['port']; - $comment = $validated['comment'] ?? ''; - $comment = trim($comment); + if (!($isAny($to) || $isIp($to))) { + return back() + ->withErrors(['to' => 'To must be "any" or a valid IP address.']) + ->withInput(); + } + + $proto = strtolower($validated['protocol']); + $port = (int) $validated['port']; + $comment = trim($validated['comment'] ?? ''); + + // Escape comment safely for shell passing $commentEscaped = str_replace("'", "\\'", $comment); - $spec = "proto {$proto} from {$from} to {$to} port {$port}"; + + $ruleSpecParts = [ + 'proto ' . $proto, + 'from ' . $from, + 'to ' . $to, + 'port ' . $port, + ]; + + $spec = implode(' ', $ruleSpecParts); + if ($commentEscaped !== '') { $spec .= " comment '" . $commentEscaped . "'"; } + if (empty(trim($spec))) { + throw new \RuntimeException('Empty rule spec — cannot execute UFW.'); + } + + if ($validated['type'] === 'allow') { (new AddUfwRuleAction())->execute($spec); } else { - (new \App\Actions\Firewall\AddUfwDenyRuleAction())->execute($spec); + (new AddUfwDenyRuleAction())->execute($spec); } session()->flash('success', 'Rule ' . $validated['type'] . 'ed successfully.');