From 8d0f51cac24cb5bf949c5b6a221e51a150d982e3 Mon Sep 17 00:00:00 2001
From: Michael R Sweet <msweet@msweet.org>
Date: Tue, 31 Mar 2026 14:45:13 -0400
Subject: [PATCH] Filter out control characters from option values.

---
 CHANGES.md      |  2 ++
 scheduler/job.c | 41 +++++++++++++++++++++++++++++++++++------
 2 files changed, 37 insertions(+), 6 deletions(-)

#diff --git a/CHANGES.md b/CHANGES.md
#index 207789345..2d45a370c 100644
#--- a/CHANGES.md
#+++ b/CHANGES.md
#@@ -9,6 +9,8 @@ Changes in CUPS v2.4.17 (YYYY-MM-DD)
#   insensitive.
# - CVE-2026-NNNNN: The RSS notifier could write outside the scheduler's RSS
#   directory.
#+- CVE-2026-NNNNN: The scheduler did not filter control characters from option
#+  values.
# - The scheduler followed symbolic links when cleaning out its temporary
#   directory (Issue #1448)
# - Updated `cupsFileGetConf` and `cupsFilePutConf` to escape more characters.
--- a/scheduler/job.c
+++ b/scheduler/job.c
@@ -4146,9 +4146,21 @@ get_options(cupsd_job_t *job,		/* I - Jo
 	  case IPP_TAG_URI :
 	      for (valptr = attr->values[i].string.text; *valptr;)
 	      {
-	        if (strchr(" \t\n\\\'\"", *valptr))
-		  *optptr++ = '\\';
-		*optptr++ = *valptr++;
+	       /*
+	        * Convert tabs and newlines to spaces, filter out control chars,
+	        * and escape \, ', and ".
+	        */
+
+	        if (isspace(*valptr & 255))
+	        {
+	          *optptr++ = ' ';
+	        }
+	        else if ((*valptr & 255) >= ' ' && *valptr != 0x7f)
+	        {
+	          if (strchr("\\\'\"", *valptr))
+		    *optptr++ = '\\';
+		  *optptr++ = *valptr++;
+		}
 	      }
 
 	      *optptr = '\0';
@@ -5407,13 +5419,30 @@ update_job(cupsd_job_t *job)		/* I - Job
     else if (loglevel == CUPSD_LOG_PPD)
     {
      /*
-      * Set attribute(s)...
+      * Set PPD keyword(s)/value(s)...
       */
 
+      int		i,		/* Looping var */
+			num_keywords;	/* Number of keywords */
+      cups_option_t	*keywords,	/* Keywords */
+			*keyword;	/* Current keyword */
+
       cupsdLogJob(job, CUPSD_LOG_DEBUG, "PPD: %s", message);
 
-      job->num_keywords = cupsParseOptions(message, job->num_keywords,
-                                           &job->keywords);
+      keywords     = NULL;
+      num_keywords = cupsParseOptions(message, 0, &keywords);
+
+      for (i = 0, keyword = keywords; i < num_keywords; i ++)
+      {
+       /*
+        * Filter out "special" PPD keywords...
+        */
+
+        if (strcmp(keyword->name, "cupsFilter") && strcmp(keyword->name, "cupsFilter2") && strcmp(keyword->name, "cupsFinishingTemplate") && strcmp(keyword->name, "cupsIPPFinishings") && strcmp(keyword->name, "cupsIPPReason") && strcmp(keyword->name, "cupsMarkerName") && strcmp(keyword->name, "cupsMaxSize") && strncmp(keyword->name, "cupsMediaQualifier", 18) && strcmp(keyword->name, "cupsMinSize") && strcmp(keyword->name, "cupsPageSizeCategory") && strcmp(keyword->name, "cupsPortMonitor") && strcmp(keyword->name, "cupsPreFilter") && strcmp(keyword->name, "cupsPrintQuality") && strcmp(keyword->name, "APPrinterPreset"))
+          job->num_keywords = cupsAddOption(keyword->name, keyword->value, job->num_keywords, &job->keywords);
+      }
+
+      cupsFreeOptions(num_keywords, keywords);
     }
     else
     {
