CVE-2023-24808: Loop with Unreachable Exit Condition ('Infinite Loop') (CWE-835)
value:  PDFio is a C library for reading and writing PDF files. In versions prior to 1.1.0 a denial of service (DOS) vulnerability exists in the pdfio parser. Crafted pdf files can cause the program to run at 100% utilization and never terminate. The pdf which causes this crash found in testing is about 28kb in size and was discovered via fuzzing. Anyone who uses this library either as a standalone binary or as a library can be DOSed when attempting to parse this type of file. Web servers or other automated processes which rely on this code to turn pdf submissions into plaintext can be DOSed when an attacker uploads the pdf. Please see the linked GHSA for an example pdf. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
CVSS v3 base score: 6.5
Published: 2023-02-07T01:15Z
Repository: https://github.com/michaelrsweet/pdfio
Language: C
Files changed: CHANGES.md, Makefile, NOTICE, pdfio-common.c, pdfio-dict.c, pdfio-file.c, pdfio-object.c, pdfio-token.c, pdfio-value.c
Commit date: 2023-02-03 20:39:04 -0500
Commit message: Fix denial-of-service attack when reading corrupt PDF files.

Diff: diff --git a/CHANGES.md b/CHANGES.md
index 8254505..b38141b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,12 +1,13 @@
 Changes in PDFio
 ================
 
 
-v1.1.0 (Month DD, YYYY)
------------------------
+v1.1.0 (February 3, 2023)
+-------------------------
 
+- CVE-2023-nnnn: Fixed a potential denial-of-service with corrupt PDF files.
 - Added `pdfioFileCreateTemporary` function (Issue #29)
 - Added `pdfioDictIterateKeys` function (Issue #31)
 - Added `pdfioContentPathEnd` function.
 - Added protection against opening multiple streams in the same file at the
   same time.
diff --git a/Makefile b/Makefile
index 4fbf69c..ac576c9 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,9 @@
 #
 # Makefile for PDFio.
 #
-# Copyright © 2021-2022 by Michael R Sweet.
+# Copyright © 2021-2023 by Michael R Sweet.
 #
 # Licensed under Apache License v2.0.  See the file "LICENSE" for more
 # information.
 #
 
@@ -27,11 +27,11 @@ DSO		=	cc
 DSOFLAGS	=
 DSONAME		=
 LDFLAGS		=
 LIBS		=	-lm -lz
 RANLIB		=	ranlib
-VERSION		=	1.1
+VERSION		=	1.1.0
 prefix		=	/usr/local
 
 
 # Base rules
 .SUFFIXES:	.c .h .o
diff --git a/NOTICE b/NOTICE
index fb3ab29..37ef3c8 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,8 +1,8 @@
 PDFio - PDF Read/Write Library
 
-Copyright © 2021-2022 by Michael R Sweet.
+Copyright © 2021-2023 by Michael R Sweet.
 
 (Optional) Exceptions to the Apache 2.0 License:
 ================================================
 
 As an exception, if, as a result of your compiling your source code, portions
diff --git a/pdfio-common.c b/pdfio-common.c
index 00da8e6..bc23fd0 100644
--- a/pdfio-common.c
+++ b/pdfio-common.c
@@ -1,9 +1,9 @@
 //
 // Common support functions for pdfio.
 //
-// Copyright © 2021 by Michael R Sweet.
+// Copyright © 2021-2023 by Michael R Sweet.
 //
 // Licensed under Apache License v2.0.  See the file "LICENSE" for more
 // information.
 //
 
@@ -36,10 +36,12 @@ _pdfioFileConsume(pdfio_file_t *pdf,	// I - PDF file
   if ((size_t)(pdf->bufend - pdf->bufptr) > bytes)
     pdf->bufptr += bytes;
   else if (_pdfioFileSeek(pdf, (off_t)bytes, SEEK_CUR) < 0)
     return (false);
 
+  PDFIO_DEBUG("_pdfioFileConsume: pos=%ld\n", (long)(pdf->bufpos + pdf->bufptr - pdf->buffer));
+
   return (true);
 }
 
 
 //
@@ -523,11 +525,11 @@ read_buffer(pdfio_file_t *pdf,		// I - PDF file
   }
 
   return (rbytes);
 }
 
-  
+
 //
 // 'write_buffer()' - Write a buffer to a PDF file.
 //
 
 static bool				// O - `true` on success and `false` on error
diff --git a/pdfio-dict.c b/pdfio-dict.c
index eaf6268..4196adf 100644
--- a/pdfio-dict.c
+++ b/pdfio-dict.c
@@ -1,9 +1,9 @@
 //
 // PDF dictionary functions for PDFio.
 //
-// Copyright © 2021-2022 by Michael R Sweet.
+// Copyright © 2021-2023 by Michael R Sweet.
 //
 // Licensed under Apache License v2.0.  See the file "LICENSE" for more
 // information.
 //
 
@@ -539,12 +539,19 @@ _pdfioDictRead(pdfio_file_t   *pdf,	// I - PDF file
     else if (key[0] != '/')
     {
       _pdfioFileError(pdf, "Invalid dictionary contents.");
       break;
     }
+    else if (_pdfioDictGetValue(dict, key + 1))
+    {
+      _pdfioFileError(pdf, "Duplicate dictionary key '%s'.", key + 1);
+      return (NULL);
+    }
 
     // Then get the next value...
+    PDFIO_DEBUG("_pdfioDictRead: Reading value for '%s'.\n", key + 1);
+
     if (!_pdfioValueRead(pdf, obj, tb, &value, depth))
     {
       _pdfioFileError(pdf, "Missing value for dictionary key.");
       break;
     }
@@ -930,13 +937,13 @@ _pdfioDictSetValue(
   if (dict->num_pairs > 1 && compare_pairs(pair - 1, pair) > 0)
     qsort(dict->pairs, dict->num_pairs, sizeof(_pdfio_pair_t), (int (*)(const void *, const void *))compare_pairs);
 
 #ifdef DEBUG
   PDFIO_DEBUG("_pdfioDictSetValue(%p): %lu pairs\n", (void *)dict, (unsigned long)dict->num_pairs);
-  PDFIO_DEBUG("_pdfioDictSetValue(%p): ", (void *)dict);
-  PDFIO_DEBUG_DICT(dict);
-  PDFIO_DEBUG("\n");
+//  PDFIO_DEBUG("_pdfioDictSetValue(%p): ", (void *)dict);
+//  PDFIO_DEBUG_DICT(dict);
+//  PDFIO_DEBUG("\n");
 #endif // DEBUG
 
   return (true);
 }
 
diff --git a/pdfio-file.c b/pdfio-file.c
index 90986d9..a95378c 100644
--- a/pdfio-file.c
+++ b/pdfio-file.c
@@ -1,9 +1,9 @@
 //
 // PDF file functions for PDFio.
 //
-// Copyright © 2021-2022 by Michael R Sweet.
+// Copyright © 2021-2023 by Michael R Sweet.
 //
 // Licensed under Apache License v2.0.  See the file "LICENSE" for more
 // information.
 //
 
@@ -1982,10 +1982,12 @@ load_xref(
       {
 	_pdfioFileError(pdf, "Trailer is not a dictionary.");
 	return (false);
       }
 
+      PDFIO_DEBUG("load_xref: Got trailer dict.\n");
+
       _pdfioTokenFlush(&tb);
 
       if (!pdf->trailer_dict)
       {
 	// Save the trailer dictionary and grab the root (catalog) and info
diff --git a/pdfio-object.c b/pdfio-object.c
index 6053518..88ab25f 100644
--- a/pdfio-object.c
+++ b/pdfio-object.c
@@ -1,9 +1,9 @@
 //
 // PDF object functions for PDFio.
 //
-// Copyright © 2021-2022 by Michael R Sweet.
+// Copyright © 2021-2023 by Michael R Sweet.
 //
 // Licensed under Apache License v2.0.  See the file "LICENSE" for more
 // information.
 //
 
diff --git a/pdfio-token.c b/pdfio-token.c
index 1331bb9..559a7ff 100644
--- a/pdfio-token.c
+++ b/pdfio-token.c
@@ -1,9 +1,9 @@
 //
 // PDF token parsing functions for PDFio.
 //
-// Copyright © 2021 by Michael R Sweet.
+// Copyright © 2021-2023 by Michael R Sweet.
 //
 // Licensed under Apache License v2.0.  See the file "LICENSE" for more
 // information.
 //
 
@@ -127,13 +127,24 @@ _pdfioTokenGet(_pdfio_token_t *tb,	// I - Token buffer/stack
 {
   // See if we have a token waiting on the stack...
   if (tb->num_tokens > 0)
   {
     // Yes, return it...
+    size_t len;				// Length of token
+
     tb->num_tokens --;
-    strncpy(buffer, tb->tokens[tb->num_tokens], bufsize - 1);
-    buffer[bufsize - 1] = '\0';
+
+    if ((len = strlen(tb->tokens[tb->num_tokens])) > (bufsize - 1))
+    {
+      // Value too large...
+      PDFIO_DEBUG("_pdfioTokenGet(tb=%p, buffer=%p, bufsize=%u): Token '%s' from stack too large.\n", tb, buffer, (unsigned)bufsize, tb->tokens[tb->num_tokens]);
+      *buffer = '\0';
+      return (false);
+    }
+
+    memcpy(buffer, tb->tokens[tb->num_tokens], len);
+    buffer[len] = '\0';
 
     PDFIO_DEBUG("_pdfioTokenGet(tb=%p, buffer=%p, bufsize=%u): Popping '%s' from stack.\n", tb, buffer, (unsigned)bufsize, buffer);
 
     free(tb->tokens[tb->num_tokens]);
     tb->tokens[tb->num_tokens] = NULL;
@@ -534,11 +545,11 @@ _pdfioTokenRead(_pdfio_token_t *tb,	// I - Token buffer/stack
 	break;
   }
 
   *bufptr = '\0';
 
-  PDFIO_DEBUG("_pdfioTokenRead: Read '%s'.\n", buffer);
+//  PDFIO_DEBUG("_pdfioTokenRead: Read '%s'.\n", buffer);
 
   return (bufptr > buffer);
 }
 
 
@@ -571,10 +582,11 @@ get_char(_pdfio_token_t *tb)		// I - Token buffer
 
     // Update pointers...
     tb->bufptr = tb->buffer;
     tb->bufend = tb->buffer + bytes;
 
+#if 0
 #ifdef DEBUG
     unsigned char *ptr;			// Pointer into buffer
 
     PDFIO_DEBUG("get_char: Read '");
     for (ptr = tb->buffer; ptr < tb->bufend; ptr ++)
@@ -584,10 +596,11 @@ get_char(_pdfio_token_t *tb)		// I - Token buffer
       else
         PDFIO_DEBUG("%c", *ptr);
     }
     PDFIO_DEBUG("'\n");
 #endif // DEBUG
+#endif // 0
   }
 
   // Return the next character...
   return (*(tb->bufptr)++);
 }
diff --git a/pdfio-value.c b/pdfio-value.c
index 7f114e6..1007465 100644
--- a/pdfio-value.c
+++ b/pdfio-value.c
@@ -1,9 +1,9 @@
 //
 // PDF value functions for PDFio.
 //
-// Copyright © 2021 by Michael R Sweet.
+// Copyright © 2021-2023 by Michael R Sweet.
 //
 // Licensed under Apache License v2.0.  See the file "LICENSE" for more
 // information.
 //
 

Vulnerable code: v1.1.0 (Month DD, YYYY)
# Copyright © 2021-2022 by Michael R Sweet.
VERSION		=	1.1
Copyright © 2021-2022 by Michael R Sweet.
// Copyright © 2021 by Michael R Sweet.
  
// Copyright © 2021-2022 by Michael R Sweet.
  PDFIO_DEBUG("_pdfioDictSetValue(%p): ", (void *)dict);
  PDFIO_DEBUG_DICT(dict);
  PDFIO_DEBUG("\n");
// Copyright © 2021-2022 by Michael R Sweet.
// Copyright © 2021-2022 by Michael R Sweet.
// Copyright © 2021 by Michael R Sweet.
    strncpy(buffer, tb->tokens[tb->num_tokens], bufsize - 1);
    buffer[bufsize - 1] = '\0';
  PDFIO_DEBUG("_pdfioTokenRead: Read '%s'.\n", buffer);
// Copyright © 2021 by Michael R Sweet.
Fixed code: v1.1.0 (February 3, 2023)
-------------------------
- CVE-2023-nnnn: Fixed a potential denial-of-service with corrupt PDF files.
# Copyright © 2021-2023 by Michael R Sweet.
VERSION		=	1.1.0
Copyright © 2021-2023 by Michael R Sweet.
// Copyright © 2021-2023 by Michael R Sweet.
  PDFIO_DEBUG("_pdfioFileConsume: pos=%ld\n", (long)(pdf->bufpos + pdf->bufptr - pdf->buffer));


// Copyright © 2021-2023 by Michael R Sweet.
    else if (_pdfioDictGetValue(dict, key + 1))
    {
      _pdfioFileError(pdf, "Duplicate dictionary key '%s'.", key + 1);
      return (NULL);
    }
    PDFIO_DEBUG("_pdfioDictRead: Reading value for '%s'.\n", key + 1);

//  PDFIO_DEBUG("_pdfioDictSetValue(%p): ", (void *)dict);
//  PDFIO_DEBUG_DICT(dict);
//  PDFIO_DEBUG("\n");
// Copyright © 2021-2023 by Michael R Sweet.
      PDFIO_DEBUG("load_xref: Got trailer dict.\n");

// Copyright © 2021-2023 by Michael R Sweet.
// Copyright © 2021-2023 by Michael R Sweet.
    size_t len;				// Length of token


    if ((len = strlen(tb->tokens[tb->num_tokens])) > (bufsize - 1))
    {
      // Value too large...
      PDFIO_DEBUG("_pdfioTokenGet(tb=%p, buffer=%p, bufsize=%u): Token '%s' from stack too large.\n", tb, buffer, (unsigned)bufsize, tb->tokens[tb->num_tokens]);
      *buffer = '\0';
      return (false);
    }

    memcpy(buffer, tb->tokens[tb->num_tokens], len);
    buffer[len] = '\0';
//  PDFIO_DEBUG("_pdfioTokenRead: Read '%s'.\n", buffer);
#if 0
#endif // 0
// Copyright © 2021-2023 by Michael R Sweet.