diff --git a/include/qpdf/Constants.h b/include/qpdf/Constants.h index 8d0306b..81b22da 100644 --- a/include/qpdf/Constants.h +++ b/include/qpdf/Constants.h @@ -287,6 +287,9 @@ enum qpdf_param_e { qpdf_p_parser_max_container_size, qpdf_p_parser_max_container_size_damaged, + /* stream and filter limits */ + qpdf_p_max_stream_filters = 0x14000, + /* next section = 0x20000 */ qpdf_enum_max = 0x7fffffff, }; diff --git a/include/qpdf/auto_job_c_global.hh b/include/qpdf/auto_job_c_global.hh index 8a2dc6b..7f8758b 100644 --- a/include/qpdf/auto_job_c_global.hh +++ b/include/qpdf/auto_job_c_global.hh @@ -10,3 +10,4 @@ QPDF_DLL GlobalConfig* parserMaxContainerSize(std::string const& parameter); QPDF_DLL GlobalConfig* parserMaxContainerSizeDamaged(std::string const& parameter); QPDF_DLL GlobalConfig* parserMaxErrors(std::string const& parameter); QPDF_DLL GlobalConfig* parserMaxNesting(std::string const& parameter); +QPDF_DLL GlobalConfig* maxStreamFilters(std::string const& parameter); diff --git a/include/qpdf/global.hh b/include/qpdf/global.hh index 360dcc6..c78e43e 100644 --- a/include/qpdf/global.hh +++ b/include/qpdf/global.hh @@ -56,7 +56,7 @@ namespace qpdf::global /// @brief Retrieves the number of limit errors. /// - /// Returns the number a global limit was exceeded. This item is read only. + /// Returns the number of times a global limit was exceeded. This item is read only. /// /// @return The number of limit errors. /// @@ -229,6 +229,34 @@ namespace qpdf::global { set_uint32(qpdf_p_parser_max_container_size_damaged, value); } + + /// @brief Retrieves the maximum number of filters allowed when filtering streams. + /// + /// An excessive number of stream filters is usually a sign that a file is damaged or + /// specially constructed. If the maximum is exceeded for a stream the stream is treated as + /// unfilterable. The default maximum is 25. + /// + /// @return The maximum number of filters allowed when filtering streams. + /// + /// @since 12.3 + uint32_t inline max_stream_filters() + { + return get_uint32(qpdf_p_max_stream_filters); + } + + /// @brief Sets the maximum number of filters allowed when filtering streams. + /// + /// An excessive number of stream filters is usually a sign that a file is damaged or + /// specially constructed. If the maximum is exceeded for a stream the stream is treated as + /// unfilterable. The default maximum is 25. + /// + /// @param value The maximum number of filters allowed when filtering streams to set. + /// + /// @since 12.3 + void inline max_stream_filters(uint32_t value) + { + set_uint32(qpdf_p_max_stream_filters, value); + } } // namespace limits } // namespace qpdf::global diff --git a/job.sums b/job.sums index d2b52d8..fb4d7b4 100644 --- a/job.sums +++ b/job.sums @@ -4,18 +4,18 @@ generate_auto_job 8e3175a515aa8837d8a01bba0346b04b3d777d70330ba5b7d52f691316054a include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 -include/qpdf/auto_job_c_global.hh f1dc365206d033a0d6b19b6e561cc244fbd5b49a8d9604b5b646a5fd92895a5a +include/qpdf/auto_job_c_global.hh 7df0ff87d18d7fa6d57437960377509420b6b6eb9527b534996f86d3bd7a0ddc include/qpdf/auto_job_c_main.hh b865eb827356554763bb8349eadfcbc5cb260f80e025a5e229467c525007356d include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 -job.yml 131922d22086d9f4710743e18229cc1e956268197bcae8e1aae30f3be42877be +job.yml fa98c8444c8a22a89aeeb76670aa5919aa7a86ebfac2eb45018602fbc7e45b79 libqpdf/qpdf/auto_job_decl.hh d612a02839e4f20a80e1c6a3ba09c17187fccddc3581ec7ebb1e3919ffd6801d -libqpdf/qpdf/auto_job_help.hh 00ac90c621b6c0529d7bad9ea596f57595517901c8d33f49d2812fbea52dfb41 -libqpdf/qpdf/auto_job_init.hh 889dde948e0ab53616584976d9520ab7ab3773c787d241f8a107f5e2f9f2112f +libqpdf/qpdf/auto_job_help.hh 7503b1083c952ace12976857c8ece4e1af67788af9f827fb4248bd22329f93cd +libqpdf/qpdf/auto_job_init.hh 10a697528d4cae1ac566ee7612f62e611190b3c10c0021862a77fa7e4f330570 libqpdf/qpdf/auto_job_json_decl.hh 7dbb83ddadcea39bfd1faa4ca061e1e3c3134d693b8ae634b463e7e19dc8bd0a -libqpdf/qpdf/auto_job_json_init.hh 3c5f3d07a85e89dd7ecd79342c18e1f0ad580fc57758abb434aa9c9ae277c01e -libqpdf/qpdf/auto_job_schema.hh eb21c99d3a4dc40b333fd1b19d5de52f8813c74a1d4ca830ea4c3311c120d63e +libqpdf/qpdf/auto_job_json_init.hh e9cacbcb78ca250a962c226a935067ef9b76f5485bae7e5302eea0a1a8e2ff65 +libqpdf/qpdf/auto_job_schema.hh 2b974a436c5b4d03fb38258d6213f993cfa9f673834cebe754b4c7ad657481c9 manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 -manual/cli.rst 08e9e7a18d2b0d05102a072f82eabf9ede6bfb1fb797be307ea680eed93ea60f -manual/qpdf.1 19a45f8de6b7c0584fe4395c4ae98b92147a2875e45dbdf729c70e644ccca295 +manual/cli.rst 0b0f6a1d8ec523751d91999586bca1356abd8f17e207bc0139ce5d7dfd64fdb4 +manual/qpdf.1 c1d6e58e37aed1b8d434b37edd1837b7261c9933b09d64bf3915dc3f35d6cccb manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b diff --git a/job.yml b/job.yml index 5e560f6..9a0752a 100644 --- a/job.yml +++ b/job.yml @@ -95,6 +95,7 @@ options: parser-max-container-size-damaged: level parser-max-errors: level parser-max-nesting: level + max-stream-filters: level - table: main config: c_main manual: @@ -419,6 +420,7 @@ json: parser-max-container-size-damaged: parser-max-errors: parser-max-nesting: + max-stream-filters: # other options update-from-json: allow-weak-crypto: diff --git a/libqpdf/QPDFJob_config.cc b/libqpdf/QPDFJob_config.cc index 273b5cb..7cb41f5 100644 --- a/libqpdf/QPDFJob_config.cc +++ b/libqpdf/QPDFJob_config.cc @@ -1216,6 +1216,13 @@ QPDFJob::GlobalConfig::parserMaxNesting(const std::string& parameter) return this; } +QPDFJob::GlobalConfig* +QPDFJob::GlobalConfig::maxStreamFilters(const std::string& parameter) +{ + global::Limits::max_stream_filters(to_uint32("max-stream-filters", parameter)); + return this; +} + QPDFJob::Config* QPDFJob::Config::setPageLabels(const std::vector& specs) { diff --git a/libqpdf/QPDF_Stream.cc b/libqpdf/QPDF_Stream.cc index f77b6e3..07f4f0e 100644 --- a/libqpdf/QPDF_Stream.cc +++ b/libqpdf/QPDF_Stream.cc @@ -513,6 +513,13 @@ Stream::filterable( // No filters return true; } + if (filter_obj.size() > global::Limits::max_stream_filters()) { + global::Limits::error(); + warn( + "limits error(max-stream-filters): too many filters for stream; treating stream as " + "not filterable"); + return false; + } if (filter_obj.isName()) { // One filter auto ff = s->filter_factory(filter_obj.getName()); diff --git a/libqpdf/global.cc b/libqpdf/global.cc index 4762ee2..058fe2a 100644 --- a/libqpdf/global.cc +++ b/libqpdf/global.cc @@ -28,6 +28,9 @@ Limits::disable_defaults() if (!l.parser_max_container_size_damaged_set_) { l.parser_max_container_size_damaged_ = std::numeric_limits::max(); } + if (!l.max_stream_filters_set_) { + l.max_stream_filters_ = std::numeric_limits::max(); + } } qpdf_result_e @@ -56,6 +59,9 @@ qpdf_global_get_uint32(qpdf_param_e param, uint32_t* value) case qpdf_p_parser_max_container_size_damaged: *value = Limits::parser_max_container_size(true); return qpdf_r_ok; + case qpdf_p_max_stream_filters: + *value = Limits::max_stream_filters(); + return qpdf_r_ok; default: return qpdf_r_bad_parameter; } @@ -83,6 +89,9 @@ qpdf_global_set_uint32(qpdf_param_e param, uint32_t value) case qpdf_p_parser_max_container_size_damaged: Limits::parser_max_container_size(true, value); return qpdf_r_ok; + case qpdf_p_max_stream_filters: + Limits::max_stream_filters(value); + return qpdf_r_ok; default: return qpdf_r_bad_parameter; } diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh index 84c8e45..a977864 100644 --- a/libqpdf/qpdf/auto_job_help.hh +++ b/libqpdf/qpdf/auto_job_help.hh @@ -1044,6 +1044,13 @@ See also --parser-max-container-size. } static void add_help_9(QPDFArgParser& ap) { +ap.addOptionHelp("--max-stream-filters", "global", "set the maximum number of filters allowed when filtering streams", R"(--max-stream-filters=n + +An excessive number of stream filters is usually a sign that a file +is damaged or specially constructed. If the maximum is exceeded for +a stream the stream is treated as unfilterable. +The default limit is 25. +)"); ap.addHelpTopic("testing", "options for testing or debugging", R"(The options below are useful when writing automated test code that includes files created by qpdf or when testing qpdf itself. )"); diff --git a/libqpdf/qpdf/auto_job_init.hh b/libqpdf/qpdf/auto_job_init.hh index 3f38cfb..89fe752 100644 --- a/libqpdf/qpdf/auto_job_init.hh +++ b/libqpdf/qpdf/auto_job_init.hh @@ -40,6 +40,7 @@ this->ap.addRequiredParameter("parser-max-container-size", [this](std::string co this->ap.addRequiredParameter("parser-max-container-size-damaged", [this](std::string const& x){c_global->parserMaxContainerSizeDamaged(x);}, "level"); this->ap.addRequiredParameter("parser-max-errors", [this](std::string const& x){c_global->parserMaxErrors(x);}, "level"); this->ap.addRequiredParameter("parser-max-nesting", [this](std::string const& x){c_global->parserMaxNesting(x);}, "level"); +this->ap.addRequiredParameter("max-stream-filters", [this](std::string const& x){c_global->maxStreamFilters(x);}, "level"); this->ap.selectMainOptionTable(); this->ap.addPositional(p(&ArgParser::argPositional)); this->ap.addBare("add-attachment", b(&ArgParser::argAddAttachment)); diff --git a/libqpdf/qpdf/auto_job_json_init.hh b/libqpdf/qpdf/auto_job_json_init.hh index 270d4b5..432977d 100644 --- a/libqpdf/qpdf/auto_job_json_init.hh +++ b/libqpdf/qpdf/auto_job_json_init.hh @@ -289,6 +289,9 @@ popHandler(); // key: parserMaxErrors pushKey("parserMaxNesting"); addParameter([this](std::string const& p) { c_global->parserMaxNesting(p); }); popHandler(); // key: parserMaxNesting +pushKey("maxStreamFilters"); +addParameter([this](std::string const& p) { c_global->maxStreamFilters(p); }); +popHandler(); // key: maxStreamFilters popHandler(); // key: global pushKey("updateFromJson"); addParameter([this](std::string const& p) { c_main->updateFromJson(p); }); diff --git a/libqpdf/qpdf/auto_job_schema.hh b/libqpdf/qpdf/auto_job_schema.hh index 839bcfd..f198b8a 100644 --- a/libqpdf/qpdf/auto_job_schema.hh +++ b/libqpdf/qpdf/auto_job_schema.hh @@ -95,7 +95,8 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({ "parserMaxContainerSize": "set the maximum container size while parsing", "parserMaxContainerSizeDamaged": "set the maximum container size while parsing damaged files", "parserMaxErrors": "set the maximum number of errors while parsing", - "parserMaxNesting": "set the maximum nesting level while parsing objects" + "parserMaxNesting": "set the maximum nesting level while parsing objects", + "maxStreamFilters": "set the maximum number of filters allowed when filtering streams" }, "updateFromJson": "update a PDF from qpdf JSON", "allowWeakCrypto": "allow insecure cryptographic algorithms", diff --git a/libqpdf/qpdf/global_private.hh b/libqpdf/qpdf/global_private.hh index 37c26e8..22da06d 100644 --- a/libqpdf/qpdf/global_private.hh +++ b/libqpdf/qpdf/global_private.hh @@ -48,6 +48,19 @@ namespace qpdf::global static void parser_max_container_size(bool damaged, uint32_t value); + static uint32_t const& + max_stream_filters() + { + return l.max_stream_filters_; + } + + static void + max_stream_filters(uint32_t value) + { + l.max_stream_filters_set_ = true; + l.max_stream_filters_ = value; + } + /// Record a limit error. static void error() @@ -79,6 +92,8 @@ namespace qpdf::global uint32_t parser_max_container_size_{std::numeric_limits::max()}; uint32_t parser_max_container_size_damaged_{5'000}; bool parser_max_container_size_damaged_set_{false}; + uint32_t max_stream_filters_{25}; + bool max_stream_filters_set_{false}; }; class Options diff --git a/libtests/objects.cc b/libtests/objects.cc index c9020b7..a52aae8 100644 --- a/libtests/objects.cc +++ b/libtests/objects.cc @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -167,6 +168,7 @@ test_2(QPDF& pdf, char const* arg2) assert(parser_max_errors() == 15); assert(parser_max_container_size() == std::numeric_limits::max()); assert(parser_max_container_size_damaged() == 5'000); + assert(max_stream_filters() == 25); assert(default_limits()); // Test disabling optional default limits @@ -175,6 +177,7 @@ test_2(QPDF& pdf, char const* arg2) assert(parser_max_errors() == 0); assert(parser_max_container_size() == std::numeric_limits::max()); assert(parser_max_container_size_damaged() == std::numeric_limits::max()); + assert(max_stream_filters() == std::numeric_limits::max()); assert(!default_limits()); // Check disabling default limits is irreversible @@ -186,11 +189,13 @@ test_2(QPDF& pdf, char const* arg2) parser_max_errors(12); parser_max_container_size(13); parser_max_container_size_damaged(14); + max_stream_filters(15); assert(parser_max_nesting() == 11); assert(parser_max_errors() == 12); assert(parser_max_container_size() == 13); assert(parser_max_container_size_damaged() == 14); + assert(max_stream_filters() == 15); // Check disabling default limits does not override explicit limits default_limits(false); @@ -198,6 +203,7 @@ test_2(QPDF& pdf, char const* arg2) assert(parser_max_errors() == 12); assert(parser_max_container_size() == 13); assert(parser_max_container_size_damaged() == 14); + assert(max_stream_filters() == 15); // Test parameter checking QUtil::handle_result_code(qpdf_r_ok, ""); @@ -239,6 +245,19 @@ test_2(QPDF& pdf, char const* arg2) } assert(qpdf::global::limit_errors() == 2); + // Test max_stream_filters + QPDF qpdf; + qpdf.emptyPDF(); + auto s = qpdf.newStream("\x01\x01\x01A"); + s.getDict().replace("/Filter", Array({Name("/RL"), Name("/RL"), Name("/RL")})); + Pl_Discard p; + auto x = s.pipeStreamData(&p, 0, qpdf_dl_all, true); + assert(x); + max_stream_filters(2); + assert(!s.pipeStreamData(&p, 0, qpdf_dl_all, true)); + max_stream_filters(3); + assert(s.pipeStreamData(&p, 0, qpdf_dl_all, true)); + // Test global settings using the QPDFJob interface QPDFJob j; j.config() @@ -248,14 +267,16 @@ test_2(QPDF& pdf, char const* arg2) ->parserMaxErrors("112") ->parserMaxContainerSize("113") ->parserMaxContainerSizeDamaged("114") + ->maxStreamFilters("115") ->noDefaultLimits() ->endGlobal() ->outputFile("a.pdf"); - auto qpdf = j.createQPDF(); + auto qpdf_uptr = j.createQPDF(); assert(parser_max_nesting() == 111); assert(parser_max_errors() == 112); assert(parser_max_container_size() == 113); assert(parser_max_container_size_damaged() == 114); + assert(max_stream_filters() == 115); assert(!default_limits()); // Test global settings using the JobJSON @@ -268,16 +289,18 @@ test_2(QPDF& pdf, char const* arg2) "parserMaxErrors": "212", "parserMaxContainerSize": "213", "parserMaxContainerSizeDamaged": "214", + "maxStreamFilters": "215", "noDefaultLimits": "" }, "outputFile": "a.pdf" } )"); - qpdf = j.createQPDF(); + qpdf_uptr = jj.createQPDF(); assert(parser_max_nesting() == 211); assert(parser_max_errors() == 212); assert(parser_max_container_size() == 213); assert(parser_max_container_size_damaged() == 214); + assert(max_stream_filters() == 215); assert(!default_limits()); } diff --git a/libtests/qtest/objects.test b/libtests/qtest/objects.test index 4959f37..ceceb61 100644 --- a/libtests/qtest/objects.test +++ b/libtests/qtest/objects.test @@ -20,12 +20,12 @@ $td->runtest("integer type checks", $td->runtest("dictionary checks", {$td->COMMAND => "objects 1 -"}, - {$td->STRING => => "test 1 done\n", $td->EXIT_STATUS => 0}, + {$td->STRING => "test 1 done\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); -$td->runtest("global object limits", +$td->runtest("global limits", {$td->COMMAND => "objects 2 -"}, - {$td->STRING => => "test 2 done\n", $td->EXIT_STATUS => 0}, + {$td->FILE => "test2.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); $td->report($n_tests); diff --git a/libtests/qtest/objects/test2.out b/libtests/qtest/objects/test2.out new file mode 100644 index 0000000..8253828 --- /dev/null +++ b/libtests/qtest/objects/test2.out @@ -0,0 +1,2 @@ +WARNING: empty PDF: limits error(max-stream-filters): too many filters for stream; treating stream as not filterable +test 2 done diff --git a/manual/cli.rst b/manual/cli.rst index e68b348..e61a1d5 100644 --- a/manual/cli.rst +++ b/manual/cli.rst @@ -3822,7 +3822,6 @@ Parser Limits Set the maximum nesting level while parsing objects. The maximum nesting level is not disabled by :qpdf:ref:`--no-default-limits`. Defaults to 499. - .. qpdf:option:: --parser-max-errors=n .. help: set the maximum number of errors while parsing @@ -3848,7 +3847,6 @@ parsing. The limit applies when the PDF document's xref table is undamaged and the object itself can be parsed without errors. The default limit is 4,294,967,295. See also :qpdf:ref:`--parser-max-container-size-damaged`. - .. qpdf:option:: --parser-max-container-size-damaged=n .. 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 xref streams. The default limit is 5,000. See also :qpdf:ref:`--parser-max-container-size`. + +Stream and Filter Limits +......................... + +.. qpdf:option:: --max-stream-filters=n + + .. help: set the maximum number of filters allowed when filtering streams + + An excessive number of stream filters is usually a sign that a file + is damaged or specially constructed. If the maximum is exceeded for + a stream the stream is treated as unfilterable. + The default limit is 25. + +Set the maximum number of filters allowed when filtering streams. An excessive +number of stream filters is usually a sign that a file is damaged or specially +constructed. If the maximum is exceeded for a stream the stream is treated as +unfilterable. The default limit is 25. + + .. _test-options: Options for Testing or Debugging diff --git a/manual/qpdf.1 b/manual/qpdf.1 index 0bfb1d0..7fe991d 100644 --- a/manual/qpdf.1 +++ b/manual/qpdf.1 @@ -1236,6 +1236,14 @@ parsing. The limit applies when the PDF document's xref table is damaged or the object itself is damaged. The limit also applies when parsing xref streams. The default limit is 5,000. See also --parser-max-container-size. +.TP +.B --max-stream-filters \-\- set the maximum number of filters allowed when filtering streams +--max-stream-filters=n + +An excessive number of stream filters is usually a sign that a file +is damaged or specially constructed. If the maximum is exceeded for +a stream the stream is treated as unfilterable. +The default limit is 25. .SH TESTING (options for testing or debugging) The options below are useful when writing automated test code that includes files created by qpdf or when testing qpdf itself. diff --git a/manual/release-notes.rst b/manual/release-notes.rst index 1b1530a..f566a20 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -106,6 +106,12 @@ more detail. - Other changes + - By default, streams with more than 25 filters are now treated as unfilterable. + A large number of filters typically occur in damaged or specially constructed + files and can cause excessive use of resources and/or stack overflows. The + limit can be changed if necessary with the new :qpdf:ref:`--max-stream-filters` + CLI option or the new ``qpdf::global::max_stream_filters`` function. + - When running in a FIPS environment using the GnuTLS crypto provider, calls to GnuTLS now use 'LAX' mode as the use of weak algorithms is required to decrypt existing files and is specified by the PDF standards