From ad0277ff175bdacf7ed541b75b1f9fe4bab1d173 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Tue, 27 Jan 2026 08:20:33 +0100 Subject: [PATCH] Enhance input validation, unicode handling, and sanitization across department goal criteria - Add `validateAndSanitize` method to `goals_criteria` for input validation and defaulting incorrect values. - Ensure JSON encoding in `response` and database interactions respects Unicode (`JSON_UNESCAPED_UNICODE`). - Sanitize and validate input for `department_goals_o::add` and criteria usage. - Fix header character encoding in response (`Content-Type: application/json; charset=utf-8`). - Update criteria `label` sanitization with trimming, length limits, and safe character handling. --- services/nginx/app/classes/response.php | 4 +- .../modules/goals/classes/goals_criteria.php | 40 ++++++++++++++++++- .../nginx/app/objects/department_goals_o.php | 9 ++++- .../nginx/app/routes/departmentGoalsRoute.php | 4 +- services/nginx/app/traits/db_object_t.php | 4 +- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/services/nginx/app/classes/response.php b/services/nginx/app/classes/response.php index 9eaf0b6c..f6bb61db 100644 --- a/services/nginx/app/classes/response.php +++ b/services/nginx/app/classes/response.php @@ -23,7 +23,7 @@ class response implements response_i #[NoReturn] public function response(bool $success, mixed $data, int $status = null): void { global $DEBUG; - header('Content-Type: application/json'); + header('Content-Type: application/json; charset=utf-8'); if ($status) { http_response_code($status); } else { @@ -52,7 +52,7 @@ class response implements response_i 'data' => $data, 'meta' => $this->meta, 'includes' => $this->includes - ]); + ], JSON_UNESCAPED_UNICODE); exit; } diff --git a/services/nginx/app/modules/goals/classes/goals_criteria.php b/services/nginx/app/modules/goals/classes/goals_criteria.php index a2d098bf..5f99cebc 100644 --- a/services/nginx/app/modules/goals/classes/goals_criteria.php +++ b/services/nginx/app/modules/goals/classes/goals_criteria.php @@ -2,6 +2,7 @@ namespace goals\classes; +use classes\db; use goals\helpers\goals_criteria_type; use goals\helpers\goals_criteria_progress_alert_frequency; use goals\interfaces\goals_criteria_i; @@ -230,6 +231,7 @@ class goals_criteria implements goals_criteria_i */ public function toArray(): array { + global /** @var db $db */ $db; // Normalize lists to identifiers only $users = $this->users?->listCustomerNumbers() ?? []; $departments = $this->departments?->listIDs() ?? []; @@ -243,7 +245,7 @@ class goals_criteria implements goals_criteria_i return [ 'type' => $this->type?->name ?? goals_criteria_type::NONE->name, 'target' => $this->target ?? 0, - 'label' => $this->label, + 'label' => (string)$this->label, 'start' => ($this->start instanceof \DateTimeInterface) ? $this->start->format(DATE_ATOM) : null, 'end' => ($this->end instanceof \DateTimeInterface) ? $this->end->format(DATE_ATOM) : null, 'users' => $users, @@ -335,4 +337,40 @@ class goals_criteria implements goals_criteria_i { return $this->getProgress(); } + + public function validateAndSanitize(): void + { + // Ensure target is non-negative + if ($this->target < 0) { + $this->target = 0; + } + + // Ensure timeframe is valid + if (($this->start instanceof \DateTimeInterface) && ($this->end instanceof \DateTimeInterface)) { + if ($this->end < $this->start) { + $tmp = $this->start; + $this->start = $this->end; + $this->end = $tmp; + } + } + + // Ensure unknown enum values are reset to defaults + if (!in_array($this->type, goals_criteria_type::cases())) { + $this->type = goals_criteria_type::NONE; + } + if (!in_array($this->progress_alert_frequency, goals_criteria_progress_alert_frequency::cases())) { + $this->progress_alert_frequency = goals_criteria_progress_alert_frequency::NONE; + } + + // Sanitize label + if (is_string($this->label)) { + $label = trim($this->label); + $label = strip_tags($label); + $label = preg_replace('/\s+/', ' ', $label); + if ($label !== null) { + $label = mb_substr($label, 0, 255); + $this->label = $label; + } + } + } } \ No newline at end of file diff --git a/services/nginx/app/objects/department_goals_o.php b/services/nginx/app/objects/department_goals_o.php index 18ef60cd..8c472202 100644 --- a/services/nginx/app/objects/department_goals_o.php +++ b/services/nginx/app/objects/department_goals_o.php @@ -36,11 +36,18 @@ class department_goals_o extends db */ public function add(users_o $created_by, array $departments, goals_criteria $criteria): department_goals_o { + // Sanitize the input + $created_by->requireSelected(); + foreach ($departments as $dept) { + $dept->requireSelected(); + } + // Validate and sanitize criteria object + $criteria->validateAndSanitize(); // Add the object $tmp_id = self::add_object([ 'created_by' => (int)$created_by->id, 'departments' => array_map(fn($dept) => (int)$dept->id, $departments), - 'criteria' => $criteria->toArray(), + 'criteria' => (array)$criteria->toArray(), ]); $this->id = $tmp_id; self::getObjectProperties(); diff --git a/services/nginx/app/routes/departmentGoalsRoute.php b/services/nginx/app/routes/departmentGoalsRoute.php index 681ca9d2..9a13f0b8 100644 --- a/services/nginx/app/routes/departmentGoalsRoute.php +++ b/services/nginx/app/routes/departmentGoalsRoute.php @@ -116,10 +116,8 @@ class departmentGoalsRoute // Parse criteria (preserve unicode characters like æ, ø, å) $criteria = goals_criteria::fromJson(json_encode($criteriaInput, JSON_UNESCAPED_UNICODE)); - - // Create + // Create the goal $goal = (new department_goals_o())->add($user, $departments, $criteria); - $response->success($goal->asArray(), 201); }, [ 'goals_department_create' => 'Create a new department goal' diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index 4a7bc11e..931b5934 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -1101,7 +1101,7 @@ trait db_object_t foreach ( $data as $key => $value ) { // JSON encode objects and arrays if (is_object($value) || is_array($value)) { - $value = json_encode($value); + $value = json_encode($value, JSON_UNESCAPED_UNICODE); if ($value === false) { throw new Exception('Failed to encode value for key: ' . $key . ' - ' . json_last_error_msg()); } @@ -1226,7 +1226,7 @@ trait db_object_t foreach ( $data as $key => $value ) { // JSON encode objects and arrays if (is_object($value) || is_array($value)) { - $value = json_encode($value); + $value = json_encode($value, JSON_UNESCAPED_UNICODE); if ($value === false) { throw new Exception('Failed to encode value for key: ' . $key . ' - ' . json_last_error_msg()); }