From 5c09dc11150caff78531fc4f32c0f2e2e0bdb395 Mon Sep 17 00:00:00 2001
From: "Jory A. Pratt" <geekypenguin@gmail.com>
Date: Sun, 15 Feb 2026 21:56:01 -0600
Subject: [PATCH 3/8] security: sanitize raw_line in cron manipulation scripts

- Add cron_helpers.inc with validate_raw_line()
- Reject raw_line containing newlines (injection prevention)
- Limit length to 1024 chars
- Add cron field validation in update_announcement.php
---
 cron_helpers.inc        | 23 +++++++++++++++++++++++
 delete_announcement.php |  8 ++++----
 toggle_cron.php         |  6 +++---
 update_announcement.php | 31 +++++++++++++++++++++++--------
 4 files changed, 53 insertions(+), 15 deletions(-)
 create mode 100644 cron_helpers.inc

diff --git a/cron_helpers.inc b/cron_helpers.inc
new file mode 100644
index 0000000..b5ce450
--- /dev/null
+++ b/cron_helpers.inc
@@ -0,0 +1,23 @@
+<?php
+/**
+ * cron_helpers.inc - Validation helpers for cron-related operations
+ */
+
+/**
+ * Validate and return sanitized raw_line from POST.
+ * Rejects newlines (injection), excessive length, empty.
+ * Returns sanitized string or null if invalid.
+ */
+function validate_raw_line(?string $raw): ?string {
+    if ($raw === null) {
+        return null;
+    }
+    $trimmed = trim($raw);
+    if ($trimmed === '' || strlen($trimmed) > 1024) {
+        return null;
+    }
+    if (strpos($raw, "\n") !== false || strpos($raw, "\r") !== false) {
+        return null;
+    }
+    return $trimmed;
+}
diff --git a/delete_announcement.php b/delete_announcement.php
index 4b8acdc..e22a0c0 100644
--- a/delete_announcement.php
+++ b/delete_announcement.php
@@ -1,14 +1,14 @@
 <?php
 
 require_once __DIR__ . '/auth_check.inc';
+require_once __DIR__ . '/cron_helpers.inc';
 
-if (!isset($_POST['raw_line'])) {
-    echo "Error: Missing cron line";
+$raw = validate_raw_line($_POST['raw_line'] ?? null);
+if ($raw === null) {
+    echo "Error: Missing or invalid cron line";
     exit;
 }
 
-$raw = trim($_POST['raw_line']);
-
 // Get current crontab
 $output = [];
 exec('sudo crontab -l', $output);
diff --git a/toggle_cron.php b/toggle_cron.php
index c329b3e..d027266 100644
--- a/toggle_cron.php
+++ b/toggle_cron.php
@@ -2,6 +2,7 @@
 // toggle_cron.php - Enable/Disable a cron job by commenting/uncommenting the line
 
 require_once __DIR__ . '/auth_check.inc';
+require_once __DIR__ . '/cron_helpers.inc';
 
 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
     http_response_code(405);
@@ -9,12 +10,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
     exit;
 }
 
-if (empty($_POST['raw_line']) || !isset($_POST['enable'])) {
+$raw_line = validate_raw_line($_POST['raw_line'] ?? null);
+if ($raw_line === null || !isset($_POST['enable'])) {
     echo "Missing parameters.";
     exit;
 }
-
-$raw_line = trim($_POST['raw_line']);
 $enable   = (bool)$_POST['enable'];  // true = uncomment (enable), false = comment (disable)
 
 // Read current crontab
diff --git a/update_announcement.php b/update_announcement.php
index a462fe6..a4353e8 100644
--- a/update_announcement.php
+++ b/update_announcement.php
@@ -7,6 +7,7 @@
  */
 
 require_once __DIR__ . '/auth_check.inc';
+require_once __DIR__ . '/cron_helpers.inc';
 
 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
     http_response_code(405);
@@ -14,20 +15,34 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
     exit;
 }
 
-$raw_line = trim($_POST['raw_line'] ?? '');
-$min      = trim($_POST['min']      ?? '');
-$hour     = trim($_POST['hour']     ?? '');
-$dom      = trim($_POST['dom']      ?? '');
-$month    = trim($_POST['month']    ?? '');
-$dow      = trim($_POST['dow']      ?? '');
-$week     = trim($_POST['week']     ?? '*');
+$raw_line = validate_raw_line($_POST['raw_line'] ?? null);
+$min      = trim((string)($_POST['min']      ?? ''));
+$hour     = trim((string)($_POST['hour']     ?? ''));
+$dom      = trim((string)($_POST['dom']      ?? ''));
+$month    = trim((string)($_POST['month']    ?? ''));
+$dow      = trim((string)($_POST['dow']      ?? ''));
+$week     = trim((string)($_POST['week']     ?? '*'));
 $use_nth  = !empty($_POST['use_nth']) && $_POST['use_nth'] == 1;
 
-if (!$raw_line || $min === '' || $hour === '' || $dom === '' || $month === '' || $dow === '') {
+if ($raw_line === null || $raw_line === '' || $min === '' || $hour === '' || $dom === '' || $month === '' || $dow === '') {
     echo "Missing required fields.";
     exit;
 }
 
+$cron_field_pattern = '/^[\d*,\-\/\s]{1,50}$/';
+$validate_cron_field = static function ($val, $name) use ($cron_field_pattern) {
+    if ($val === '' || !preg_match($cron_field_pattern, $val)) {
+        return false;
+    }
+    return true;
+};
+if (!$validate_cron_field($min, 'min') || !$validate_cron_field($hour, 'hour')
+    || !$validate_cron_field($dom, 'dom') || !$validate_cron_field($month, 'month')
+    || !$validate_cron_field($dow, 'dow')) {
+    echo "Invalid cron field values.";
+    exit;
+}
+
 // Read current crontab
 $output = [];
 $retval = 0;
-- 
2.47.3

