CVE-2015-9252 (published 2018-02-13T19:29Z)

An issue was discovered in QPDF before 7.0.0. Endless recursion causes stack exhaustion in QPDFTokenizer::resolveLiteral() in QPDFTokenizer.cc, related to the QPDF::resolve function in QPDF.cc.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 5.5, MEDIUM (CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-399
cwe_name: Resource Management Errors
description: Weaknesses in this category are related to improper management of system resources.

Fix information:
repo_url: https://github.com/qpdf/qpdf
commit_details: repo_url: https://github.com/qpdf/qpdf
author: Jay Berkenbilt
author_date: 2017-07-26 05:03:38-04:00
msg: Detect recursion loops resolving objects (fixes #51)

During parsing of an object, sometimes parts of the object have to be
resolved. An example is stream lengths. If such an object directly or
indirectly points to the object being parsed, it can cause an infinite
loop. Guard against all cases of re-entrant resolution of objects.
repository: repo_url: https://github.com/qpdf/qpdf
repo_name: qpdf/qpdf
description: qpdf: A content-preserving PDF document transformer
repo_language: C++
owner: qpdf
file_changes: filename: ChangeLog
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,10 @@
 2017-07-26  Jay Berkenbilt  <ejb@ql.org>
 
+	* Detect infinite loops while resolving objects. This could happen
+	if something inside an object that had to be resolved during
+	parsing, such as a stream length, recursively referenced the
+	object being resolved.
+
 	* CVE-2017-9208: Handle references to and appearance of object 0
 	as a special case. Object 0 is not allowed, and qpdf was using it
 	internally to represent direct objects.

programming_language: Markdown
filename: QPDF.hh
change_type: ModificationType.MODIFY
diff: @@ -603,6 +603,25 @@ class QPDF
         int gen;
     };
 
+    class ResolveRecorder
+    {
+      public:
+        ResolveRecorder(QPDF* qpdf, QPDFObjGen const& og) :
+            qpdf(qpdf),
+            og(og)
+        {
+            qpdf->resolving.insert(og);
+        }
+        virtual ~ResolveRecorder()
+        {
+            this->qpdf->resolving.erase(og);
+        }
+      private:
+        QPDF* qpdf;
+        QPDFObjGen og;
+    };
+    friend class ResolveRecorder;
+
     void parse(char const* password);
     void warn(QPDFExc const& e);
     void setTrailer(QPDFObjectHandle obj);
@@ -1065,6 +1084,7 @@ class QPDF
     std::map<QPDFObjGen, QPDFXRefEntry> xref_table;
     std::set<int> deleted_objects;
     std::map<QPDFObjGen, ObjCache> obj_cache;
+    std::set<QPDFObjGen> resolving;
     QPDFObjectHandle trailer;
     std::vector<QPDFObjectHandle> all_pages;
     std::map<QPDFObjGen, int> pageobj_to_pages_pos;

programming_language: C++
filename: QPDF.cc
change_type: ModificationType.MODIFY
diff: @@ -1471,6 +1471,21 @@ QPDF::resolve(int objid, int generation)
     // to insert things into the object cache that don't actually
     // exist in the file.
     QPDFObjGen og(objid, generation);
+    if (this->resolving.count(og))
+    {
+        // This can happen if an object references itself directly or
+        // indirectly in some key that has to be resolved during
+        // object parsing, such as stream length.
+	QTC::TC("qpdf", "QPDF recursion loop in resolve");
+	warn(QPDFExc(qpdf_e_damaged_pdf, this->file->getName(),
+		     "", this->file->getLastOffset(),
+		     "loop detected resolving object " +
+		     QUtil::int_to_string(objid) + " " +
+		     QUtil::int_to_string(generation)));
+        return new QPDF_Null;
+    }
+    ResolveRecorder rr(this, og);
+
     if (! this->obj_cache.count(og))
     {
 	if (! this->xref_table.count(og))

programming_language: C++
filename: qpdf.testcov
change_type: ModificationType.MODIFY
diff: @@ -276,3 +276,4 @@ qpdf-c called qpdf_set_deterministic_ID 0
 QPDFObjectHandle indirect with 0 objid 0
 QPDF object id 0 0
 QPDF caught recursive xref reconstruction 0
+QPDF recursion loop in resolve 0

programming_language: Markdown
filename: qpdf.test
change_type: ModificationType.MODIFY
diff: @@ -206,7 +206,7 @@ $td->runtest("remove page we don't have",
 show_ntests();
 # ----------
 $td->notify("--- Miscellaneous Tests ---");
-$n_tests += 81;
+$n_tests += 82;
 
 $td->runtest("qpdf version",
 	     {$td->COMMAND => "qpdf --version"},
@@ -220,6 +220,7 @@ $td->runtest("C API: qpdf version",
 
 # Files to reproduce various bugs
 foreach my $d (
+    ["51", "resolve loop"],
     ["99", "object 0"],
     ["99b", "object 0"],
     ["100","xref reconstruction loop"],

programming_language: Perl
filename: issue-51.out
change_type: ModificationType.ADD
diff: @@ -0,0 +1,6 @@
+WARNING: issue-51.pdf: reported number of objects (0) inconsistent with actual number of objects (9)
+WARNING: issue-51.pdf (object 7 0, file position 553): expected endobj
+WARNING: issue-51.pdf (object 1 0, file position 359): expected endobj
+WARNING: issue-51.pdf (file position 70): loop detected resolving object 2 0
+WARNING: issue-51.pdf (object 2 0, file position 71): attempting to recover stream length
+issue-51.pdf (object 2 0, file position 71): unable to recover stream data

programming_language: Markdown
filename: issue-51.pdf
change_type: ModificationType.ADD
diff: @@ -0,0 +1,22 @@
+%PDF-100000000000002 0 obj
+<</Length 2 0 R/000000/00000000000>>
+stream
+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 0 obj
+<</0000000000000000 0 0 R/000000000 0 0 R/00000000[00000000000]/00000<</0/00000000000000000000000000000000>>/00000000 2 0 R>>000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007 0 obj
+<</0000/0000000/00000 0 0 R
+/0000000000[1 0 R 0000 null null 0]
+/0000(00000)
+>>0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000xref
+0 9
+0000000000 00000 f 
+0000000200 00000 n 
+0000000009 00000 n 
+0000000000 00000 n 
+0000000000 00000 n 
+0000000000 00000 n 
+0000000000 00000 n 
+0000000400 00000 n 
+0000000000 00000 n 
+trailer<</Size 0/Root 7 0 R>>startxref
+740
+%%EOF
\ No newline at end of file

programming_language: Erlang