Commit 701b518d5c56a1449825a3a37a716c58e05e1c3e

Authored by Jay Berkenbilt
1 parent afe0242b

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.
ChangeLog
1 1 2017-07-26 Jay Berkenbilt <ejb@ql.org>
2 2  
  3 + * Detect infinite loops while resolving objects. This could happen
  4 + if something inside an object that had to be resolved during
  5 + parsing, such as a stream length, recursively referenced the
  6 + object being resolved.
  7 +
3 8 * CVE-2017-9208: Handle references to and appearance of object 0
4 9 as a special case. Object 0 is not allowed, and qpdf was using it
5 10 internally to represent direct objects.
... ...
include/qpdf/QPDF.hh
... ... @@ -603,6 +603,25 @@ class QPDF
603 603 int gen;
604 604 };
605 605  
  606 + class ResolveRecorder
  607 + {
  608 + public:
  609 + ResolveRecorder(QPDF* qpdf, QPDFObjGen const& og) :
  610 + qpdf(qpdf),
  611 + og(og)
  612 + {
  613 + qpdf->resolving.insert(og);
  614 + }
  615 + virtual ~ResolveRecorder()
  616 + {
  617 + this->qpdf->resolving.erase(og);
  618 + }
  619 + private:
  620 + QPDF* qpdf;
  621 + QPDFObjGen og;
  622 + };
  623 + friend class ResolveRecorder;
  624 +
606 625 void parse(char const* password);
607 626 void warn(QPDFExc const& e);
608 627 void setTrailer(QPDFObjectHandle obj);
... ... @@ -1065,6 +1084,7 @@ class QPDF
1065 1084 std::map<QPDFObjGen, QPDFXRefEntry> xref_table;
1066 1085 std::set<int> deleted_objects;
1067 1086 std::map<QPDFObjGen, ObjCache> obj_cache;
  1087 + std::set<QPDFObjGen> resolving;
1068 1088 QPDFObjectHandle trailer;
1069 1089 std::vector<QPDFObjectHandle> all_pages;
1070 1090 std::map<QPDFObjGen, int> pageobj_to_pages_pos;
... ...
libqpdf/QPDF.cc
... ... @@ -1471,6 +1471,21 @@ QPDF::resolve(int objid, int generation)
1471 1471 // to insert things into the object cache that don't actually
1472 1472 // exist in the file.
1473 1473 QPDFObjGen og(objid, generation);
  1474 + if (this->resolving.count(og))
  1475 + {
  1476 + // This can happen if an object references itself directly or
  1477 + // indirectly in some key that has to be resolved during
  1478 + // object parsing, such as stream length.
  1479 + QTC::TC("qpdf", "QPDF recursion loop in resolve");
  1480 + warn(QPDFExc(qpdf_e_damaged_pdf, this->file->getName(),
  1481 + "", this->file->getLastOffset(),
  1482 + "loop detected resolving object " +
  1483 + QUtil::int_to_string(objid) + " " +
  1484 + QUtil::int_to_string(generation)));
  1485 + return new QPDF_Null;
  1486 + }
  1487 + ResolveRecorder rr(this, og);
  1488 +
1474 1489 if (! this->obj_cache.count(og))
1475 1490 {
1476 1491 if (! this->xref_table.count(og))
... ...
qpdf/qpdf.testcov
... ... @@ -276,3 +276,4 @@ qpdf-c called qpdf_set_deterministic_ID 0
276 276 QPDFObjectHandle indirect with 0 objid 0
277 277 QPDF object id 0 0
278 278 QPDF caught recursive xref reconstruction 0
  279 +QPDF recursion loop in resolve 0
... ...
qpdf/qtest/qpdf.test
... ... @@ -206,7 +206,7 @@ $td-&gt;runtest(&quot;remove page we don&#39;t have&quot;,
206 206 show_ntests();
207 207 # ----------
208 208 $td->notify("--- Miscellaneous Tests ---");
209   -$n_tests += 81;
  209 +$n_tests += 82;
210 210  
211 211 $td->runtest("qpdf version",
212 212 {$td->COMMAND => "qpdf --version"},
... ... @@ -220,6 +220,7 @@ $td-&gt;runtest(&quot;C API: qpdf version&quot;,
220 220  
221 221 # Files to reproduce various bugs
222 222 foreach my $d (
  223 + ["51", "resolve loop"],
223 224 ["99", "object 0"],
224 225 ["99b", "object 0"],
225 226 ["100","xref reconstruction loop"],
... ...
qpdf/qtest/qpdf/issue-51.out 0 → 100644
  1 +WARNING: issue-51.pdf: reported number of objects (0) inconsistent with actual number of objects (9)
  2 +WARNING: issue-51.pdf (object 7 0, file position 553): expected endobj
  3 +WARNING: issue-51.pdf (object 1 0, file position 359): expected endobj
  4 +WARNING: issue-51.pdf (file position 70): loop detected resolving object 2 0
  5 +WARNING: issue-51.pdf (object 2 0, file position 71): attempting to recover stream length
  6 +issue-51.pdf (object 2 0, file position 71): unable to recover stream data
... ...
qpdf/qtest/qpdf/issue-51.pdf 0 → 100644
  1 +%PDF-100000000000002 0 obj
  2 +<</Length 2 0 R/000000/00000000000>>
  3 +stream
  4 +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 0 obj
  5 +<</0000000000000000 0 0 R/000000000 0 0 R/00000000[00000000000]/00000<</0/00000000000000000000000000000000>>/00000000 2 0 R>>000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007 0 obj
  6 +<</0000/0000000/00000 0 0 R
  7 +/0000000000[1 0 R 0000 null null 0]
  8 +/0000(00000)
  9 +>>0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000xref
  10 +0 9
  11 +0000000000 00000 f
  12 +0000000200 00000 n
  13 +0000000009 00000 n
  14 +0000000000 00000 n
  15 +0000000000 00000 n
  16 +0000000000 00000 n
  17 +0000000000 00000 n
  18 +0000000400 00000 n
  19 +0000000000 00000 n
  20 +trailer<</Size 0/Root 7 0 R>>startxref
  21 +740
  22 +%%EOF
0 23 \ No newline at end of file
... ...