Commit 898c2943f8b876a51bf497dfc56c5186dfd6be04

Authored by m-holger
1 parent a3fca12d

Introduce `--max-stream-filters` to limit filters on streams

Add a new configurable limit `--max-stream-filters` to address excessive stream filters in damaged or specially constructed PDFs. Update related documentation, tests, and release notes to reflect this feature.
include/qpdf/Constants.h
@@ -287,6 +287,9 @@ enum qpdf_param_e { @@ -287,6 +287,9 @@ enum qpdf_param_e {
287 qpdf_p_parser_max_container_size, 287 qpdf_p_parser_max_container_size,
288 qpdf_p_parser_max_container_size_damaged, 288 qpdf_p_parser_max_container_size_damaged,
289 289
  290 + /* stream and filter limits */
  291 + qpdf_p_max_stream_filters = 0x14000,
  292 +
290 /* next section = 0x20000 */ 293 /* next section = 0x20000 */
291 qpdf_enum_max = 0x7fffffff, 294 qpdf_enum_max = 0x7fffffff,
292 }; 295 };
include/qpdf/auto_job_c_global.hh
@@ -10,3 +10,4 @@ QPDF_DLL GlobalConfig* parserMaxContainerSize(std::string const& parameter); @@ -10,3 +10,4 @@ QPDF_DLL GlobalConfig* parserMaxContainerSize(std::string const& parameter);
10 QPDF_DLL GlobalConfig* parserMaxContainerSizeDamaged(std::string const& parameter); 10 QPDF_DLL GlobalConfig* parserMaxContainerSizeDamaged(std::string const& parameter);
11 QPDF_DLL GlobalConfig* parserMaxErrors(std::string const& parameter); 11 QPDF_DLL GlobalConfig* parserMaxErrors(std::string const& parameter);
12 QPDF_DLL GlobalConfig* parserMaxNesting(std::string const& parameter); 12 QPDF_DLL GlobalConfig* parserMaxNesting(std::string const& parameter);
  13 +QPDF_DLL GlobalConfig* maxStreamFilters(std::string const& parameter);
include/qpdf/global.hh
@@ -56,7 +56,7 @@ namespace qpdf::global @@ -56,7 +56,7 @@ namespace qpdf::global
56 56
57 /// @brief Retrieves the number of limit errors. 57 /// @brief Retrieves the number of limit errors.
58 /// 58 ///
59 - /// Returns the number a global limit was exceeded. This item is read only. 59 + /// Returns the number of times a global limit was exceeded. This item is read only.
60 /// 60 ///
61 /// @return The number of limit errors. 61 /// @return The number of limit errors.
62 /// 62 ///
@@ -229,6 +229,34 @@ namespace qpdf::global @@ -229,6 +229,34 @@ namespace qpdf::global
229 { 229 {
230 set_uint32(qpdf_p_parser_max_container_size_damaged, value); 230 set_uint32(qpdf_p_parser_max_container_size_damaged, value);
231 } 231 }
  232 +
  233 + /// @brief Retrieves the maximum number of filters allowed when filtering streams.
  234 + ///
  235 + /// An excessive number of stream filters is usually a sign that a file is damaged or
  236 + /// specially constructed. If the maximum is exceeded for a stream the stream is treated as
  237 + /// unfilterable. The default maximum is 25.
  238 + ///
  239 + /// @return The maximum number of filters allowed when filtering streams.
  240 + ///
  241 + /// @since 12.3
  242 + uint32_t inline max_stream_filters()
  243 + {
  244 + return get_uint32(qpdf_p_max_stream_filters);
  245 + }
  246 +
  247 + /// @brief Sets the maximum number of filters allowed when filtering streams.
  248 + ///
  249 + /// An excessive number of stream filters is usually a sign that a file is damaged or
  250 + /// specially constructed. If the maximum is exceeded for a stream the stream is treated as
  251 + /// unfilterable. The default maximum is 25.
  252 + ///
  253 + /// @param value The maximum number of filters allowed when filtering streams to set.
  254 + ///
  255 + /// @since 12.3
  256 + void inline max_stream_filters(uint32_t value)
  257 + {
  258 + set_uint32(qpdf_p_max_stream_filters, value);
  259 + }
232 } // namespace limits 260 } // namespace limits
233 261
234 } // namespace qpdf::global 262 } // namespace qpdf::global
job.sums
@@ -4,18 +4,18 @@ generate_auto_job 8e3175a515aa8837d8a01bba0346b04b3d777d70330ba5b7d52f691316054a @@ -4,18 +4,18 @@ generate_auto_job 8e3175a515aa8837d8a01bba0346b04b3d777d70330ba5b7d52f691316054a
4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4
5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42
6 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 6 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5
7 -include/qpdf/auto_job_c_global.hh f1dc365206d033a0d6b19b6e561cc244fbd5b49a8d9604b5b646a5fd92895a5a 7 +include/qpdf/auto_job_c_global.hh 7df0ff87d18d7fa6d57437960377509420b6b6eb9527b534996f86d3bd7a0ddc
8 include/qpdf/auto_job_c_main.hh b865eb827356554763bb8349eadfcbc5cb260f80e025a5e229467c525007356d 8 include/qpdf/auto_job_c_main.hh b865eb827356554763bb8349eadfcbc5cb260f80e025a5e229467c525007356d
9 include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 9 include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506
10 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 10 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62
11 -job.yml 131922d22086d9f4710743e18229cc1e956268197bcae8e1aae30f3be42877be 11 +job.yml fa98c8444c8a22a89aeeb76670aa5919aa7a86ebfac2eb45018602fbc7e45b79
12 libqpdf/qpdf/auto_job_decl.hh d612a02839e4f20a80e1c6a3ba09c17187fccddc3581ec7ebb1e3919ffd6801d 12 libqpdf/qpdf/auto_job_decl.hh d612a02839e4f20a80e1c6a3ba09c17187fccddc3581ec7ebb1e3919ffd6801d
13 -libqpdf/qpdf/auto_job_help.hh 00ac90c621b6c0529d7bad9ea596f57595517901c8d33f49d2812fbea52dfb41  
14 -libqpdf/qpdf/auto_job_init.hh 889dde948e0ab53616584976d9520ab7ab3773c787d241f8a107f5e2f9f2112f 13 +libqpdf/qpdf/auto_job_help.hh 7503b1083c952ace12976857c8ece4e1af67788af9f827fb4248bd22329f93cd
  14 +libqpdf/qpdf/auto_job_init.hh 10a697528d4cae1ac566ee7612f62e611190b3c10c0021862a77fa7e4f330570
15 libqpdf/qpdf/auto_job_json_decl.hh 7dbb83ddadcea39bfd1faa4ca061e1e3c3134d693b8ae634b463e7e19dc8bd0a 15 libqpdf/qpdf/auto_job_json_decl.hh 7dbb83ddadcea39bfd1faa4ca061e1e3c3134d693b8ae634b463e7e19dc8bd0a
16 -libqpdf/qpdf/auto_job_json_init.hh 3c5f3d07a85e89dd7ecd79342c18e1f0ad580fc57758abb434aa9c9ae277c01e  
17 -libqpdf/qpdf/auto_job_schema.hh eb21c99d3a4dc40b333fd1b19d5de52f8813c74a1d4ca830ea4c3311c120d63e 16 +libqpdf/qpdf/auto_job_json_init.hh e9cacbcb78ca250a962c226a935067ef9b76f5485bae7e5302eea0a1a8e2ff65
  17 +libqpdf/qpdf/auto_job_schema.hh 2b974a436c5b4d03fb38258d6213f993cfa9f673834cebe754b4c7ad657481c9
18 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 18 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580
19 -manual/cli.rst 08e9e7a18d2b0d05102a072f82eabf9ede6bfb1fb797be307ea680eed93ea60f  
20 -manual/qpdf.1 19a45f8de6b7c0584fe4395c4ae98b92147a2875e45dbdf729c70e644ccca295 19 +manual/cli.rst 0b0f6a1d8ec523751d91999586bca1356abd8f17e207bc0139ce5d7dfd64fdb4
  20 +manual/qpdf.1 c1d6e58e37aed1b8d434b37edd1837b7261c9933b09d64bf3915dc3f35d6cccb
21 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b 21 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b
@@ -95,6 +95,7 @@ options: @@ -95,6 +95,7 @@ options:
95 parser-max-container-size-damaged: level 95 parser-max-container-size-damaged: level
96 parser-max-errors: level 96 parser-max-errors: level
97 parser-max-nesting: level 97 parser-max-nesting: level
  98 + max-stream-filters: level
98 - table: main 99 - table: main
99 config: c_main 100 config: c_main
100 manual: 101 manual:
@@ -419,6 +420,7 @@ json: @@ -419,6 +420,7 @@ json:
419 parser-max-container-size-damaged: 420 parser-max-container-size-damaged:
420 parser-max-errors: 421 parser-max-errors:
421 parser-max-nesting: 422 parser-max-nesting:
  423 + max-stream-filters:
422 # other options 424 # other options
423 update-from-json: 425 update-from-json:
424 allow-weak-crypto: 426 allow-weak-crypto:
libqpdf/QPDFJob_config.cc
@@ -1216,6 +1216,13 @@ QPDFJob::GlobalConfig::parserMaxNesting(const std::string& parameter) @@ -1216,6 +1216,13 @@ QPDFJob::GlobalConfig::parserMaxNesting(const std::string& parameter)
1216 return this; 1216 return this;
1217 } 1217 }
1218 1218
  1219 +QPDFJob::GlobalConfig*
  1220 +QPDFJob::GlobalConfig::maxStreamFilters(const std::string& parameter)
  1221 +{
  1222 + global::Limits::max_stream_filters(to_uint32("max-stream-filters", parameter));
  1223 + return this;
  1224 +}
  1225 +
1219 QPDFJob::Config* 1226 QPDFJob::Config*
1220 QPDFJob::Config::setPageLabels(const std::vector<std::string>& specs) 1227 QPDFJob::Config::setPageLabels(const std::vector<std::string>& specs)
1221 { 1228 {
libqpdf/QPDF_Stream.cc
@@ -513,6 +513,13 @@ Stream::filterable( @@ -513,6 +513,13 @@ Stream::filterable(
513 // No filters 513 // No filters
514 return true; 514 return true;
515 } 515 }
  516 + if (filter_obj.size() > global::Limits::max_stream_filters()) {
  517 + global::Limits::error();
  518 + warn(
  519 + "limits error(max-stream-filters): too many filters for stream; treating stream as "
  520 + "not filterable");
  521 + return false;
  522 + }
516 if (filter_obj.isName()) { 523 if (filter_obj.isName()) {
517 // One filter 524 // One filter
518 auto ff = s->filter_factory(filter_obj.getName()); 525 auto ff = s->filter_factory(filter_obj.getName());
libqpdf/global.cc
@@ -28,6 +28,9 @@ Limits::disable_defaults() @@ -28,6 +28,9 @@ Limits::disable_defaults()
28 if (!l.parser_max_container_size_damaged_set_) { 28 if (!l.parser_max_container_size_damaged_set_) {
29 l.parser_max_container_size_damaged_ = std::numeric_limits<uint32_t>::max(); 29 l.parser_max_container_size_damaged_ = std::numeric_limits<uint32_t>::max();
30 } 30 }
  31 + if (!l.max_stream_filters_set_) {
  32 + l.max_stream_filters_ = std::numeric_limits<uint32_t>::max();
  33 + }
31 } 34 }
32 35
33 qpdf_result_e 36 qpdf_result_e
@@ -56,6 +59,9 @@ qpdf_global_get_uint32(qpdf_param_e param, uint32_t* value) @@ -56,6 +59,9 @@ qpdf_global_get_uint32(qpdf_param_e param, uint32_t* value)
56 case qpdf_p_parser_max_container_size_damaged: 59 case qpdf_p_parser_max_container_size_damaged:
57 *value = Limits::parser_max_container_size(true); 60 *value = Limits::parser_max_container_size(true);
58 return qpdf_r_ok; 61 return qpdf_r_ok;
  62 + case qpdf_p_max_stream_filters:
  63 + *value = Limits::max_stream_filters();
  64 + return qpdf_r_ok;
59 default: 65 default:
60 return qpdf_r_bad_parameter; 66 return qpdf_r_bad_parameter;
61 } 67 }
@@ -83,6 +89,9 @@ qpdf_global_set_uint32(qpdf_param_e param, uint32_t value) @@ -83,6 +89,9 @@ qpdf_global_set_uint32(qpdf_param_e param, uint32_t value)
83 case qpdf_p_parser_max_container_size_damaged: 89 case qpdf_p_parser_max_container_size_damaged:
84 Limits::parser_max_container_size(true, value); 90 Limits::parser_max_container_size(true, value);
85 return qpdf_r_ok; 91 return qpdf_r_ok;
  92 + case qpdf_p_max_stream_filters:
  93 + Limits::max_stream_filters(value);
  94 + return qpdf_r_ok;
86 default: 95 default:
87 return qpdf_r_bad_parameter; 96 return qpdf_r_bad_parameter;
88 } 97 }
libqpdf/qpdf/auto_job_help.hh
@@ -1044,6 +1044,13 @@ See also --parser-max-container-size. @@ -1044,6 +1044,13 @@ See also --parser-max-container-size.
1044 } 1044 }
1045 static void add_help_9(QPDFArgParser& ap) 1045 static void add_help_9(QPDFArgParser& ap)
1046 { 1046 {
  1047 +ap.addOptionHelp("--max-stream-filters", "global", "set the maximum number of filters allowed when filtering streams", R"(--max-stream-filters=n
  1048 +
  1049 +An excessive number of stream filters is usually a sign that a file
  1050 +is damaged or specially constructed. If the maximum is exceeded for
  1051 +a stream the stream is treated as unfilterable.
  1052 +The default limit is 25.
  1053 +)");
1047 ap.addHelpTopic("testing", "options for testing or debugging", R"(The options below are useful when writing automated test code that 1054 ap.addHelpTopic("testing", "options for testing or debugging", R"(The options below are useful when writing automated test code that
1048 includes files created by qpdf or when testing qpdf itself. 1055 includes files created by qpdf or when testing qpdf itself.
1049 )"); 1056 )");
libqpdf/qpdf/auto_job_init.hh
@@ -40,6 +40,7 @@ this-&gt;ap.addRequiredParameter(&quot;parser-max-container-size&quot;, [this](std::string co @@ -40,6 +40,7 @@ this-&gt;ap.addRequiredParameter(&quot;parser-max-container-size&quot;, [this](std::string co
40 this->ap.addRequiredParameter("parser-max-container-size-damaged", [this](std::string const& x){c_global->parserMaxContainerSizeDamaged(x);}, "level"); 40 this->ap.addRequiredParameter("parser-max-container-size-damaged", [this](std::string const& x){c_global->parserMaxContainerSizeDamaged(x);}, "level");
41 this->ap.addRequiredParameter("parser-max-errors", [this](std::string const& x){c_global->parserMaxErrors(x);}, "level"); 41 this->ap.addRequiredParameter("parser-max-errors", [this](std::string const& x){c_global->parserMaxErrors(x);}, "level");
42 this->ap.addRequiredParameter("parser-max-nesting", [this](std::string const& x){c_global->parserMaxNesting(x);}, "level"); 42 this->ap.addRequiredParameter("parser-max-nesting", [this](std::string const& x){c_global->parserMaxNesting(x);}, "level");
  43 +this->ap.addRequiredParameter("max-stream-filters", [this](std::string const& x){c_global->maxStreamFilters(x);}, "level");
43 this->ap.selectMainOptionTable(); 44 this->ap.selectMainOptionTable();
44 this->ap.addPositional(p(&ArgParser::argPositional)); 45 this->ap.addPositional(p(&ArgParser::argPositional));
45 this->ap.addBare("add-attachment", b(&ArgParser::argAddAttachment)); 46 this->ap.addBare("add-attachment", b(&ArgParser::argAddAttachment));
libqpdf/qpdf/auto_job_json_init.hh
@@ -289,6 +289,9 @@ popHandler(); // key: parserMaxErrors @@ -289,6 +289,9 @@ popHandler(); // key: parserMaxErrors
289 pushKey("parserMaxNesting"); 289 pushKey("parserMaxNesting");
290 addParameter([this](std::string const& p) { c_global->parserMaxNesting(p); }); 290 addParameter([this](std::string const& p) { c_global->parserMaxNesting(p); });
291 popHandler(); // key: parserMaxNesting 291 popHandler(); // key: parserMaxNesting
  292 +pushKey("maxStreamFilters");
  293 +addParameter([this](std::string const& p) { c_global->maxStreamFilters(p); });
  294 +popHandler(); // key: maxStreamFilters
292 popHandler(); // key: global 295 popHandler(); // key: global
293 pushKey("updateFromJson"); 296 pushKey("updateFromJson");
294 addParameter([this](std::string const& p) { c_main->updateFromJson(p); }); 297 addParameter([this](std::string const& p) { c_main->updateFromJson(p); });
libqpdf/qpdf/auto_job_schema.hh
@@ -95,7 +95,8 @@ static constexpr char const* JOB_SCHEMA_DATA = R&quot;({ @@ -95,7 +95,8 @@ static constexpr char const* JOB_SCHEMA_DATA = R&quot;({
95 "parserMaxContainerSize": "set the maximum container size while parsing", 95 "parserMaxContainerSize": "set the maximum container size while parsing",
96 "parserMaxContainerSizeDamaged": "set the maximum container size while parsing damaged files", 96 "parserMaxContainerSizeDamaged": "set the maximum container size while parsing damaged files",
97 "parserMaxErrors": "set the maximum number of errors while parsing", 97 "parserMaxErrors": "set the maximum number of errors while parsing",
98 - "parserMaxNesting": "set the maximum nesting level while parsing objects" 98 + "parserMaxNesting": "set the maximum nesting level while parsing objects",
  99 + "maxStreamFilters": "set the maximum number of filters allowed when filtering streams"
99 }, 100 },
100 "updateFromJson": "update a PDF from qpdf JSON", 101 "updateFromJson": "update a PDF from qpdf JSON",
101 "allowWeakCrypto": "allow insecure cryptographic algorithms", 102 "allowWeakCrypto": "allow insecure cryptographic algorithms",
libqpdf/qpdf/global_private.hh
@@ -48,6 +48,19 @@ namespace qpdf::global @@ -48,6 +48,19 @@ namespace qpdf::global
48 48
49 static void parser_max_container_size(bool damaged, uint32_t value); 49 static void parser_max_container_size(bool damaged, uint32_t value);
50 50
  51 + static uint32_t const&
  52 + max_stream_filters()
  53 + {
  54 + return l.max_stream_filters_;
  55 + }
  56 +
  57 + static void
  58 + max_stream_filters(uint32_t value)
  59 + {
  60 + l.max_stream_filters_set_ = true;
  61 + l.max_stream_filters_ = value;
  62 + }
  63 +
51 /// Record a limit error. 64 /// Record a limit error.
52 static void 65 static void
53 error() 66 error()
@@ -79,6 +92,8 @@ namespace qpdf::global @@ -79,6 +92,8 @@ namespace qpdf::global
79 uint32_t parser_max_container_size_{std::numeric_limits<uint32_t>::max()}; 92 uint32_t parser_max_container_size_{std::numeric_limits<uint32_t>::max()};
80 uint32_t parser_max_container_size_damaged_{5'000}; 93 uint32_t parser_max_container_size_damaged_{5'000};
81 bool parser_max_container_size_damaged_set_{false}; 94 bool parser_max_container_size_damaged_set_{false};
  95 + uint32_t max_stream_filters_{25};
  96 + bool max_stream_filters_set_{false};
82 }; 97 };
83 98
84 class Options 99 class Options
libtests/objects.cc
@@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
4 4
5 #include <qpdf/QPDF.hh> 5 #include <qpdf/QPDF.hh>
6 6
  7 +#include <qpdf/Pl_Discard.hh>
7 #include <qpdf/QIntC.hh> 8 #include <qpdf/QIntC.hh>
8 #include <qpdf/QPDFJob.hh> 9 #include <qpdf/QPDFJob.hh>
9 #include <qpdf/QPDFObjectHandle_private.hh> 10 #include <qpdf/QPDFObjectHandle_private.hh>
@@ -167,6 +168,7 @@ test_2(QPDF&amp; pdf, char const* arg2) @@ -167,6 +168,7 @@ test_2(QPDF&amp; pdf, char const* arg2)
167 assert(parser_max_errors() == 15); 168 assert(parser_max_errors() == 15);
168 assert(parser_max_container_size() == std::numeric_limits<uint32_t>::max()); 169 assert(parser_max_container_size() == std::numeric_limits<uint32_t>::max());
169 assert(parser_max_container_size_damaged() == 5'000); 170 assert(parser_max_container_size_damaged() == 5'000);
  171 + assert(max_stream_filters() == 25);
170 assert(default_limits()); 172 assert(default_limits());
171 173
172 // Test disabling optional default limits 174 // Test disabling optional default limits
@@ -175,6 +177,7 @@ test_2(QPDF&amp; pdf, char const* arg2) @@ -175,6 +177,7 @@ test_2(QPDF&amp; pdf, char const* arg2)
175 assert(parser_max_errors() == 0); 177 assert(parser_max_errors() == 0);
176 assert(parser_max_container_size() == std::numeric_limits<uint32_t>::max()); 178 assert(parser_max_container_size() == std::numeric_limits<uint32_t>::max());
177 assert(parser_max_container_size_damaged() == std::numeric_limits<uint32_t>::max()); 179 assert(parser_max_container_size_damaged() == std::numeric_limits<uint32_t>::max());
  180 + assert(max_stream_filters() == std::numeric_limits<uint32_t>::max());
178 assert(!default_limits()); 181 assert(!default_limits());
179 182
180 // Check disabling default limits is irreversible 183 // Check disabling default limits is irreversible
@@ -186,11 +189,13 @@ test_2(QPDF&amp; pdf, char const* arg2) @@ -186,11 +189,13 @@ test_2(QPDF&amp; pdf, char const* arg2)
186 parser_max_errors(12); 189 parser_max_errors(12);
187 parser_max_container_size(13); 190 parser_max_container_size(13);
188 parser_max_container_size_damaged(14); 191 parser_max_container_size_damaged(14);
  192 + max_stream_filters(15);
189 193
190 assert(parser_max_nesting() == 11); 194 assert(parser_max_nesting() == 11);
191 assert(parser_max_errors() == 12); 195 assert(parser_max_errors() == 12);
192 assert(parser_max_container_size() == 13); 196 assert(parser_max_container_size() == 13);
193 assert(parser_max_container_size_damaged() == 14); 197 assert(parser_max_container_size_damaged() == 14);
  198 + assert(max_stream_filters() == 15);
194 199
195 // Check disabling default limits does not override explicit limits 200 // Check disabling default limits does not override explicit limits
196 default_limits(false); 201 default_limits(false);
@@ -198,6 +203,7 @@ test_2(QPDF&amp; pdf, char const* arg2) @@ -198,6 +203,7 @@ test_2(QPDF&amp; pdf, char const* arg2)
198 assert(parser_max_errors() == 12); 203 assert(parser_max_errors() == 12);
199 assert(parser_max_container_size() == 13); 204 assert(parser_max_container_size() == 13);
200 assert(parser_max_container_size_damaged() == 14); 205 assert(parser_max_container_size_damaged() == 14);
  206 + assert(max_stream_filters() == 15);
201 207
202 // Test parameter checking 208 // Test parameter checking
203 QUtil::handle_result_code(qpdf_r_ok, ""); 209 QUtil::handle_result_code(qpdf_r_ok, "");
@@ -239,6 +245,19 @@ test_2(QPDF&amp; pdf, char const* arg2) @@ -239,6 +245,19 @@ test_2(QPDF&amp; pdf, char const* arg2)
239 } 245 }
240 assert(qpdf::global::limit_errors() == 2); 246 assert(qpdf::global::limit_errors() == 2);
241 247
  248 + // Test max_stream_filters
  249 + QPDF qpdf;
  250 + qpdf.emptyPDF();
  251 + auto s = qpdf.newStream("\x01\x01\x01A");
  252 + s.getDict().replace("/Filter", Array({Name("/RL"), Name("/RL"), Name("/RL")}));
  253 + Pl_Discard p;
  254 + auto x = s.pipeStreamData(&p, 0, qpdf_dl_all, true);
  255 + assert(x);
  256 + max_stream_filters(2);
  257 + assert(!s.pipeStreamData(&p, 0, qpdf_dl_all, true));
  258 + max_stream_filters(3);
  259 + assert(s.pipeStreamData(&p, 0, qpdf_dl_all, true));
  260 +
242 // Test global settings using the QPDFJob interface 261 // Test global settings using the QPDFJob interface
243 QPDFJob j; 262 QPDFJob j;
244 j.config() 263 j.config()
@@ -248,14 +267,16 @@ test_2(QPDF&amp; pdf, char const* arg2) @@ -248,14 +267,16 @@ test_2(QPDF&amp; pdf, char const* arg2)
248 ->parserMaxErrors("112") 267 ->parserMaxErrors("112")
249 ->parserMaxContainerSize("113") 268 ->parserMaxContainerSize("113")
250 ->parserMaxContainerSizeDamaged("114") 269 ->parserMaxContainerSizeDamaged("114")
  270 + ->maxStreamFilters("115")
251 ->noDefaultLimits() 271 ->noDefaultLimits()
252 ->endGlobal() 272 ->endGlobal()
253 ->outputFile("a.pdf"); 273 ->outputFile("a.pdf");
254 - auto qpdf = j.createQPDF(); 274 + auto qpdf_uptr = j.createQPDF();
255 assert(parser_max_nesting() == 111); 275 assert(parser_max_nesting() == 111);
256 assert(parser_max_errors() == 112); 276 assert(parser_max_errors() == 112);
257 assert(parser_max_container_size() == 113); 277 assert(parser_max_container_size() == 113);
258 assert(parser_max_container_size_damaged() == 114); 278 assert(parser_max_container_size_damaged() == 114);
  279 + assert(max_stream_filters() == 115);
259 assert(!default_limits()); 280 assert(!default_limits());
260 281
261 // Test global settings using the JobJSON 282 // Test global settings using the JobJSON
@@ -268,16 +289,18 @@ test_2(QPDF&amp; pdf, char const* arg2) @@ -268,16 +289,18 @@ test_2(QPDF&amp; pdf, char const* arg2)
268 "parserMaxErrors": "212", 289 "parserMaxErrors": "212",
269 "parserMaxContainerSize": "213", 290 "parserMaxContainerSize": "213",
270 "parserMaxContainerSizeDamaged": "214", 291 "parserMaxContainerSizeDamaged": "214",
  292 + "maxStreamFilters": "215",
271 "noDefaultLimits": "" 293 "noDefaultLimits": ""
272 }, 294 },
273 "outputFile": "a.pdf" 295 "outputFile": "a.pdf"
274 } 296 }
275 )"); 297 )");
276 - qpdf = j.createQPDF(); 298 + qpdf_uptr = jj.createQPDF();
277 assert(parser_max_nesting() == 211); 299 assert(parser_max_nesting() == 211);
278 assert(parser_max_errors() == 212); 300 assert(parser_max_errors() == 212);
279 assert(parser_max_container_size() == 213); 301 assert(parser_max_container_size() == 213);
280 assert(parser_max_container_size_damaged() == 214); 302 assert(parser_max_container_size_damaged() == 214);
  303 + assert(max_stream_filters() == 215);
281 assert(!default_limits()); 304 assert(!default_limits());
282 } 305 }
283 306
libtests/qtest/objects.test
@@ -20,12 +20,12 @@ $td-&gt;runtest(&quot;integer type checks&quot;, @@ -20,12 +20,12 @@ $td-&gt;runtest(&quot;integer type checks&quot;,
20 20
21 $td->runtest("dictionary checks", 21 $td->runtest("dictionary checks",
22 {$td->COMMAND => "objects 1 -"}, 22 {$td->COMMAND => "objects 1 -"},
23 - {$td->STRING => => "test 1 done\n", $td->EXIT_STATUS => 0}, 23 + {$td->STRING => "test 1 done\n", $td->EXIT_STATUS => 0},
24 $td->NORMALIZE_NEWLINES); 24 $td->NORMALIZE_NEWLINES);
25 25
26 -$td->runtest("global object limits", 26 +$td->runtest("global limits",
27 {$td->COMMAND => "objects 2 -"}, 27 {$td->COMMAND => "objects 2 -"},
28 - {$td->STRING => => "test 2 done\n", $td->EXIT_STATUS => 0}, 28 + {$td->FILE => "test2.out", $td->EXIT_STATUS => 0},
29 $td->NORMALIZE_NEWLINES); 29 $td->NORMALIZE_NEWLINES);
30 30
31 $td->report($n_tests); 31 $td->report($n_tests);
libtests/qtest/objects/test2.out 0 → 100644
  1 +WARNING: empty PDF: limits error(max-stream-filters): too many filters for stream; treating stream as not filterable
  2 +test 2 done
manual/cli.rst
@@ -3822,7 +3822,6 @@ Parser Limits @@ -3822,7 +3822,6 @@ Parser Limits
3822 Set the maximum nesting level while parsing objects. The maximum nesting level is not 3822 Set the maximum nesting level while parsing objects. The maximum nesting level is not
3823 disabled by :qpdf:ref:`--no-default-limits`. Defaults to 499. 3823 disabled by :qpdf:ref:`--no-default-limits`. Defaults to 499.
3824 3824
3825 -  
3826 .. qpdf:option:: --parser-max-errors=n 3825 .. qpdf:option:: --parser-max-errors=n
3827 3826
3828 .. help: set the maximum number of errors while parsing 3827 .. help: set the maximum number of errors while parsing
@@ -3848,7 +3847,6 @@ parsing. The limit applies when the PDF document&#39;s xref table is undamaged @@ -3848,7 +3847,6 @@ parsing. The limit applies when the PDF document&#39;s xref table is undamaged
3848 and the object itself can be parsed without errors. The default limit 3847 and the object itself can be parsed without errors. The default limit
3849 is 4,294,967,295. See also :qpdf:ref:`--parser-max-container-size-damaged`. 3848 is 4,294,967,295. See also :qpdf:ref:`--parser-max-container-size-damaged`.
3850 3849
3851 -  
3852 .. qpdf:option:: --parser-max-container-size-damaged=n 3850 .. qpdf:option:: --parser-max-container-size-damaged=n
3853 3851
3854 .. help: set the maximum container size while parsing damaged files 3852 .. help: set the maximum container size while parsing damaged files
@@ -3866,6 +3864,25 @@ or the object itself is damaged. The limit also applies when parsing @@ -3866,6 +3864,25 @@ or the object itself is damaged. The limit also applies when parsing
3866 xref streams. The default limit is 5,000. 3864 xref streams. The default limit is 5,000.
3867 See also :qpdf:ref:`--parser-max-container-size`. 3865 See also :qpdf:ref:`--parser-max-container-size`.
3868 3866
  3867 +
  3868 +Stream and Filter Limits
  3869 +.........................
  3870 +
  3871 +.. qpdf:option:: --max-stream-filters=n
  3872 +
  3873 + .. help: set the maximum number of filters allowed when filtering streams
  3874 +
  3875 + An excessive number of stream filters is usually a sign that a file
  3876 + is damaged or specially constructed. If the maximum is exceeded for
  3877 + a stream the stream is treated as unfilterable.
  3878 + The default limit is 25.
  3879 +
  3880 +Set the maximum number of filters allowed when filtering streams. An excessive
  3881 +number of stream filters is usually a sign that a file is damaged or specially
  3882 +constructed. If the maximum is exceeded for a stream the stream is treated as
  3883 +unfilterable. The default limit is 25.
  3884 +
  3885 +
3869 .. _test-options: 3886 .. _test-options:
3870 3887
3871 Options for Testing or Debugging 3888 Options for Testing or Debugging
manual/qpdf.1
@@ -1236,6 +1236,14 @@ parsing. The limit applies when the PDF document&#39;s xref table is damaged @@ -1236,6 +1236,14 @@ parsing. The limit applies when the PDF document&#39;s xref table is damaged
1236 or the object itself is damaged. The limit also applies when parsing 1236 or the object itself is damaged. The limit also applies when parsing
1237 xref streams. The default limit is 5,000. 1237 xref streams. The default limit is 5,000.
1238 See also --parser-max-container-size. 1238 See also --parser-max-container-size.
  1239 +.TP
  1240 +.B --max-stream-filters \-\- set the maximum number of filters allowed when filtering streams
  1241 +--max-stream-filters=n
  1242 +
  1243 +An excessive number of stream filters is usually a sign that a file
  1244 +is damaged or specially constructed. If the maximum is exceeded for
  1245 +a stream the stream is treated as unfilterable.
  1246 +The default limit is 25.
1239 .SH TESTING (options for testing or debugging) 1247 .SH TESTING (options for testing or debugging)
1240 The options below are useful when writing automated test code that 1248 The options below are useful when writing automated test code that
1241 includes files created by qpdf or when testing qpdf itself. 1249 includes files created by qpdf or when testing qpdf itself.
manual/release-notes.rst
@@ -106,6 +106,12 @@ more detail. @@ -106,6 +106,12 @@ more detail.
106 106
107 - Other changes 107 - Other changes
108 108
  109 + - By default, streams with more than 25 filters are now treated as unfilterable.
  110 + A large number of filters typically occur in damaged or specially constructed
  111 + files and can cause excessive use of resources and/or stack overflows. The
  112 + limit can be changed if necessary with the new :qpdf:ref:`--max-stream-filters`
  113 + CLI option or the new ``qpdf::global::max_stream_filters`` function.
  114 +
109 - When running in a FIPS environment using the GnuTLS crypto provider, 115 - When running in a FIPS environment using the GnuTLS crypto provider,
110 calls to GnuTLS now use 'LAX' mode as the use of weak algorithms is 116 calls to GnuTLS now use 'LAX' mode as the use of weak algorithms is
111 required to decrypt existing files and is specified by the PDF standards 117 required to decrypt existing files and is specified by the PDF standards