Backport of:

From e9ddf7bf6c96e3488bfc57faf97b43a145cbd4ef Mon Sep 17 00:00:00 2001
From: Michael R Sweet <msweet@msweet.org>
Date: Mon, 13 Apr 2026 12:44:27 -0400
Subject: [PATCH] Protect against deep collection values (Issue #1539)

---
 CHANGES.md          |   1 +
 cups/cups-private.h |  11 ++-
 cups/dest-options.c |   4 +-
 cups/encode.c       | 179 +++++++++++++++++++++++++++-----------------
 4 files changed, 122 insertions(+), 73 deletions(-)

#diff --git a/CHANGES.md b/CHANGES.md
#index 66504a25b9..e25202ee86 100644
#--- a/CHANGES.md
#+++ b/CHANGES.md
#@@ -38,6 +38,7 @@ Changes in CUPS v2.4.17 (YYYY-MM-DD)
# - Fixed a crash bug in the rastertoepson filter.
# - Fixed a bug in cgiCheckVariables.
# - Fixed a debug printf bug on Windows (Issue #1529)
#+- Fixed a recursion issue with encoding of nested collections (Issue #1539)
# 
# 
# Changes in CUPS v2.4.16 (2025-12-04)
--- a/cups/cups-private.h
+++ b/cups/cups-private.h
@@ -40,6 +40,13 @@ extern "C" {
 
 
 /*
+ * Constants...
+ */
+
+#  define _CUPS_MAX_OPTION_DEPTH 4	/* Maximum depth of nested options/collections */
+
+
+/*
  * Types...
  */
 
@@ -269,7 +276,7 @@ extern void		_cupsBufferRelease(char *b)
 
 extern http_t		*_cupsConnect(void) _CUPS_PRIVATE;
 extern char		*_cupsCreateDest(const char *name, const char *info, const char *device_id, const char *device_uri, char *uri, size_t urisize) _CUPS_PRIVATE;
-extern ipp_attribute_t	*_cupsEncodeOption(ipp_t *ipp, ipp_tag_t group_tag, _ipp_option_t *map, const char *name, const char *value) _CUPS_PRIVATE;
+extern ipp_attribute_t	*_cupsEncodeOption(ipp_t *ipp, ipp_tag_t group_tag, _ipp_option_t *map, const char *name, const char *value, int depth) _CUPS_PRIVATE;
 extern int		_cupsGet1284Values(const char *device_id, cups_option_t **values) _CUPS_PRIVATE;
 extern const char	*_cupsGetDestResource(cups_dest_t *dest, unsigned flags, char *resource, size_t resourcesize) _CUPS_PRIVATE;
 extern int		_cupsGetDests(http_t *http, ipp_op_t op, const char *name, cups_dest_t **dests, cups_ptype_t type, cups_ptype_t mask) _CUPS_PRIVATE;
--- a/cups/dest-options.c
+++ b/cups/dest-options.c
@@ -2727,7 +2727,7 @@ cups_test_constraints(
 
         case IPP_TAG_BEGIN_COLLECTION :
             col = ippNew();
-            _cupsEncodeOption(col, IPP_TAG_ZERO, NULL, ippGetName(attr), value);
+            _cupsEncodeOption(col, IPP_TAG_ZERO, NULL, ippGetName(attr), value, /*depth*/0);
 
             for (i = 0, count = ippGetCount(attr); i < count; i ++)
             {
--- a/cups/encode.c
+++ b/cups/encode.c
@@ -375,6 +375,7 @@ static const _ipp_option_t ipp_options[]
  */
 
 static int	compare_ipp_options(_ipp_option_t *a, _ipp_option_t *b);
+static void	encode_options(ipp_t *ipp, int num_options, cups_option_t *options, ipp_tag_t group_tag, int depth);
 
 
 /*
@@ -387,7 +388,8 @@ _cupsEncodeOption(
     ipp_tag_t     group_tag,		/* I - Group tag */
     _ipp_option_t *map,			/* I - Option mapping, if any */
     const char    *name,		/* I - Attribute name */
-    const char    *value)		/* I - Value */
+    const char    *value,		/* I - Value */
+    int           depth)		/* I - Depth of values */
 {
   int			i,		/* Looping var */
 			count;		/* Number of values */
@@ -640,6 +642,19 @@ _cupsEncodeOption(
 	  * Collection value
 	  */
 
+          if (depth >= _CUPS_MAX_OPTION_DEPTH)
+          {
+           /*
+            * Don't allow "infinite" recursion of collection values...
+            */
+
+	    if (copy)
+	      free(copy);
+
+	    ippDeleteAttribute(ipp, attr);
+	    return (NULL);
+          }
+
 	  num_cols = cupsParseOptions(val, 0, &cols);
 	  if ((collection = ippNew()) == NULL)
 	  {
@@ -653,7 +668,7 @@ _cupsEncodeOption(
 	  }
 
 	  ippSetCollection(ipp, &attr, i, collection);
-	  cupsEncodeOptions2(collection, num_cols, cols, IPP_TAG_JOB);
+	  encode_options(collection, num_cols, cols, IPP_TAG_JOB, depth + 1);
 	  cupsFreeOptions(num_cols, cols);
 	  ippDelete(collection);
 	  break;
@@ -683,7 +698,7 @@ cupsEncodeOption(ipp_t      *ipp,	/* I -
                  const char *name,	/* I - Option name */
                  const char *value)	/* I - Option string value */
 {
-  return (_cupsEncodeOption(ipp, group_tag, _ippFindOption(name), name, value));
+  return (_cupsEncodeOption(ipp, group_tag, _ippFindOption(name), name, value, /*depth*/0));
 }
 
 
@@ -729,11 +744,8 @@ cupsEncodeOptions2(
     cups_option_t *options,		/* I - Options */
     ipp_tag_t     group_tag)		/* I - Group to encode */
 {
-  int			i;		/* Looping var */
   char			*val;		/* Pointer to option value */
-  cups_option_t		*option;	/* Current option */
   ipp_op_t		op;		/* Operation for this request */
-  const ipp_op_t	*ops;		/* List of allowed operations */
 
 
   DEBUG_printf(("cupsEncodeOptions2(ipp=%p(%s), num_options=%d, options=%p, group_tag=%x)", (void *)ipp, ipp ? ippOpString(ippGetOperation(ipp)) : "", num_options, (void *)options, group_tag));
@@ -766,7 +778,92 @@ cupsEncodeOptions2(
   }
 
  /*
-  * Then loop through the options...
+  * Then encode the options...
+  */
+
+  encode_options(ipp, num_options, options, group_tag, /*depth*/0);
+}
+
+
+#ifdef DEBUG
+/*
+ * '_ippCheckOptions()' - Validate that the option array is sorted properly.
+ */
+
+const char *				/* O - First out-of-order option or NULL */
+_ippCheckOptions(void)
+{
+  int	i;				/* Looping var */
+
+
+  for (i = 0; i < (int)(sizeof(ipp_options) / sizeof(ipp_options[0]) - 1); i ++)
+    if (strcmp(ipp_options[i].name, ipp_options[i + 1].name) >= 0)
+      return (ipp_options[i + 1].name);
+
+  return (NULL);
+}
+#endif /* DEBUG */
+
+
+/*
+ * '_ippFindOption()' - Find the attribute information for an option.
+ */
+
+_ipp_option_t *				/* O - Attribute information */
+_ippFindOption(const char *name)	/* I - Option/attribute name */
+{
+  _ipp_option_t	key;			/* Search key */
+
+
+ /*
+  * Lookup the proper value and group tags for this option...
+  */
+
+  key.name = name;
+
+  return ((_ipp_option_t *)bsearch(&key, ipp_options,
+                                   sizeof(ipp_options) / sizeof(ipp_options[0]),
+				   sizeof(ipp_options[0]),
+				   (int (*)(const void *, const void *))
+				       compare_ipp_options));
+}
+
+
+/*
+ * 'compare_ipp_options()' - Compare two IPP options.
+ */
+
+static int				/* O - Result of comparison */
+compare_ipp_options(_ipp_option_t *a,	/* I - First option */
+                    _ipp_option_t *b)	/* I - Second option */
+{
+  return (strcmp(a->name, b->name));
+}
+
+
+/*
+ * 'encode_options()' - Encode options to the specified depth.
+ */
+
+void
+encode_options(
+    ipp_t         *ipp,			/* I - IPP request/response */
+    int           num_options,		/* I - Number of options */
+    cups_option_t *options,		/* I - Options */
+    ipp_tag_t     group_tag,		/* I - Group to encode */
+    int           depth)		/* I - Depth of options/collections */
+{
+  int			i;		/* Looping var */
+  cups_option_t		*option;	/* Current option */
+  ipp_op_t		op = ippGetOperation(ipp);
+					/* Operation for this request */
+  const ipp_op_t	*ops;		/* List of allowed operations */
+
+
+  DEBUG_printf(("4encode_options(ipp=%p(%s), num_options=%d, options=%p, group_tag=%x, depth=%d)", (void *)ipp, ipp ? ippOpString(ippGetOperation(ipp)) : "", num_options, (void *)options, group_tag, depth));
+
+ /*
+  * Loop through the options...
   */
 
   for (i = num_options, option = options; i > 0; i --, option ++)
@@ -774,7 +871,7 @@ cupsEncodeOptions2(
     _ipp_option_t	*match;		/* Matching attribute */
 
    /*
-    * Skip document format options that are handled above...
+    * Skip document format options that are handled in cupsEncodeOptions2...
     */
 
     if (!_cups_strcasecmp(option->name, "raw") || !_cups_strcasecmp(option->name, "document-format") || !option->name[0])
@@ -801,7 +898,7 @@ cupsEncodeOptions2(
         ops = ipp_set_printer;
       else
       {
-	DEBUG_printf(("2cupsEncodeOptions2: Skipping \"%s\".", option->name));
+	DEBUG_printf(("5encode_options: Skipping \"%s\".", option->name));
         continue;
       }
     }
@@ -815,13 +912,13 @@ cupsEncodeOptions2(
       {
 	if (group_tag != IPP_TAG_JOB && group_tag != IPP_TAG_DOCUMENT)
 	{
-	  DEBUG_printf(("2cupsEncodeOptions2: Skipping \"%s\".", option->name));
+	  DEBUG_printf(("5encode_options: Skipping \"%s\".", option->name));
           continue;
         }
       }
       else if (group_tag != IPP_TAG_PRINTER)
       {
-	DEBUG_printf(("2cupsEncodeOptions2: Skipping \"%s\".", option->name));
+	DEBUG_printf(("5encode_options: Skipping \"%s\".", option->name));
         continue;
       }
 
@@ -845,66 +942,10 @@ cupsEncodeOptions2(
 
     if (*ops == IPP_OP_CUPS_NONE && op != IPP_OP_CUPS_NONE)
     {
-      DEBUG_printf(("2cupsEncodeOptions2: Skipping \"%s\".", option->name));
+      DEBUG_printf(("5encode_options: Skipping \"%s\".", option->name));
       continue;
     }
 
-    _cupsEncodeOption(ipp, group_tag, match, option->name, option->value);
+    _cupsEncodeOption(ipp, group_tag, match, option->name, option->value, depth);
   }
 }
-
-
-#ifdef DEBUG
-/*
- * '_ippCheckOptions()' - Validate that the option array is sorted properly.
- */
-
-const char *				/* O - First out-of-order option or NULL */
-_ippCheckOptions(void)
-{
-  int	i;				/* Looping var */
-
-
-  for (i = 0; i < (int)(sizeof(ipp_options) / sizeof(ipp_options[0]) - 1); i ++)
-    if (strcmp(ipp_options[i].name, ipp_options[i + 1].name) >= 0)
-      return (ipp_options[i + 1].name);
-
-  return (NULL);
-}
-#endif /* DEBUG */
-
-
-/*
- * '_ippFindOption()' - Find the attribute information for an option.
- */
-
-_ipp_option_t *				/* O - Attribute information */
-_ippFindOption(const char *name)	/* I - Option/attribute name */
-{
-  _ipp_option_t	key;			/* Search key */
-
-
- /*
-  * Lookup the proper value and group tags for this option...
-  */
-
-  key.name = name;
-
-  return ((_ipp_option_t *)bsearch(&key, ipp_options,
-                                   sizeof(ipp_options) / sizeof(ipp_options[0]),
-				   sizeof(ipp_options[0]),
-				   (int (*)(const void *, const void *))
-				       compare_ipp_options));
-}
-
-
-/*
- * 'compare_ipp_options()' - Compare two IPP options.
- */
-
-static int				/* O - Result of comparison */
-compare_ipp_options(_ipp_option_t *a,	/* I - First option */
-                    _ipp_option_t *b)	/* I - Second option */
-{
-  return (strcmp(a->name, b->name));
-}
