Skip to content

Commit 386d459

Browse files
fix(pnm,jxl): Prevent PNM/JXL readers from loading arbitrarily large non-image files (#5203)
##### PNM The PNM reader did not provide an optimized `valid_file` implementation. While `PNMInput::open` would perform basic validation of the PNM header, it would only do this after having read the entire contents of the file into memory. Besides the obvious risks/issues with having `open` unconditionally read arbitrarily large files into memory, this also meant that any call to `valid_file` would necessarily do the same (as the default implementation delegates to `open` when ioproxy support is present). To fix these issues, the following changes were made: * Provide an efficient implementation of `valid_file` for `PNMInput` that only loads the header, and then validates magic number/dimensions. * In `PNMInput::open`, first load only the header to memory, and subsequently, only load the rest of the image data if the header is parsed successfully, and the data contained within is valid. * Added a rough limit to header read size of 1KiB. * Added a rough limit to full file read size of 1GiB. Note that, for now, if the file size exceeds the 1GiB limit, the read is simply truncated to 1GiB, rather than failing altogether. ##### JPEGXL The JXL reader already provided an efficient `valid_file` override, implemented by validating the 128 byte JXL signature. The signature, however, was not being validated in `JxlInput::open` before attempting to decode the file. The process of JXL decoding appears to read the entire contents of the file to memory (at least for some non-JXL inputs). Taken in combination, this means that when `JXLInput::open` was called on arbitrarily large non-JXL inputs, the entire file was being read into memory before `open` would fail. As a simple fix for this, `JxlInput::open` now checks the result of `valid_file` before attempting any decoding. ### Tests I did not add a new testsuite case for this, but did test on my end that for both PNM and JPEGXL, large invalid files are no longer being read into memory, while valid images (of their respective types) are still read/opened successfully. --------- Signed-off-by: maxwelliverson <maxwelliverson@gmail.com>
1 parent 539aba4 commit 386d459

2 files changed

Lines changed: 184 additions & 39 deletions

File tree

src/jpegxl.imageio/jxlinput.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ JxlInput::open(const std::string& name, ImageSpec& newspec)
164164
return false;
165165
}
166166

167+
if (!valid_file(m_io)) {
168+
DBG std::cout << "JxlInput::valid_file() return false\n";
169+
errorfmt("Possible corrupt file, "
170+
"JPEG XL signature verification failed\n");
171+
return false;
172+
}
173+
167174
m_decoder = JxlDecoderMake(nullptr);
168175
if (m_decoder == nullptr) {
169176
DBG std::cout << "JxlDecoderMake failed\n";

src/pnm.imageio/pnminput.cpp

Lines changed: 177 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <cstdio>
66
#include <cstdlib>
77
#include <fstream>
8+
#include <optional>
89
#include <string>
910

1011
#include <OpenImageIO/filesystem.h>
@@ -23,6 +24,17 @@ OIIO_PLUGIN_NAMESPACE_BEGIN
2324
// http://netpbm.sourceforge.net/doc/pam.html (base format)
2425
//
2526

27+
enum PNMType { P1, P2, P3, P4, P5, P6, Pf, PF };
28+
29+
30+
31+
struct PNMBasicInfo {
32+
PNMType type;
33+
int width;
34+
int height;
35+
};
36+
37+
2638

2739
class PNMInput final : public ImageInput {
2840
public:
@@ -40,10 +52,9 @@ class PNMInput final : public ImageInput {
4052
int current_subimage(void) const override { return 0; }
4153
bool read_native_scanline(int subimage, int miplevel, int y, int z,
4254
void* data) override;
55+
bool valid_file(Filesystem::IOProxy* ioproxy) const override;
4356

4457
private:
45-
enum PNMType { P1, P2, P3, P4, P5, P6, Pf, PF };
46-
4758
PNMType m_pnm_type;
4859
int m_max_val;
4960
float m_scaling_factor;
@@ -63,17 +74,13 @@ class PNMInput final : public ImageInput {
6374
bool read_file_scanline(void* data, int y);
6475
bool read_file_header();
6576

66-
void skipComments()
67-
{
68-
while (m_remaining.size() && Strutil::parse_char(m_remaining, '#'))
69-
Strutil::parse_line(m_remaining);
70-
}
77+
static string_view read_header_to_buffer(std::vector<char>& buffer,
78+
Filesystem::IOProxy* io);
79+
static string_view append_remainder_to_buffer(std::vector<char>& buffer,
80+
Filesystem::IOProxy* io,
81+
string_view remaining);
7182

72-
template<typename T> bool nextVal(T& val)
73-
{
74-
skipComments();
75-
return Strutil::parse_value(m_remaining, val);
76-
}
83+
template<typename T> bool nextVal(T& val);
7784

7885
template<class T>
7986
bool ascii_to_raw(T* write, imagesize_t nvals, T max, bool invert = false);
@@ -104,6 +111,34 @@ OIIO_EXPORT const char* pnm_input_extensions[] = { "ppm", "pgm", "pbm",
104111
OIIO_PLUGIN_EXPORTS_END
105112

106113

114+
// 1KiB approximate limit on header size
115+
static const imagesize_t max_pnm_header_size = 1024;
116+
117+
// 1GiB rough limit on file size to avoid loading arbitrarily
118+
// large files into memory
119+
static const imagesize_t max_pnm_file_size = 1024 * 1024 * 1024;
120+
121+
122+
123+
inline static void
124+
skip_header_comments(string_view& header)
125+
{
126+
while (header.size() && Strutil::parse_char(header, '#'))
127+
Strutil::parse_line(header);
128+
}
129+
130+
131+
132+
template<typename T>
133+
inline static bool
134+
parse_next_header_value(string_view& header, T& val)
135+
{
136+
skip_header_comments(header);
137+
return Strutil::parse_value(header, val);
138+
}
139+
140+
141+
107142
template<class T>
108143
inline void
109144
invert(const T* read, T* write, imagesize_t nvals)
@@ -114,6 +149,15 @@ invert(const T* read, T* write, imagesize_t nvals)
114149

115150

116151

152+
template<typename T>
153+
bool
154+
PNMInput::nextVal(T& val)
155+
{
156+
return parse_next_header_value(m_remaining, val);
157+
}
158+
159+
160+
117161
template<class T>
118162
bool
119163
PNMInput::ascii_to_raw(T* write, imagesize_t nvals, T max, bool invert)
@@ -189,6 +233,39 @@ unpack_floats(const unsigned char* read, float* write, imagesize_t numsamples,
189233

190234

191235

236+
static std::optional<PNMBasicInfo>
237+
read_type_and_resolution(string_view& header)
238+
{
239+
PNMType type;
240+
241+
if (!Strutil::parse_char(header, 'P') || header.empty())
242+
return std::nullopt;
243+
244+
switch (header.front()) {
245+
case '1': type = P1; break;
246+
case '2': type = P2; break;
247+
case '3': type = P3; break;
248+
case '4': type = P4; break;
249+
case '5': type = P5; break;
250+
case '6': type = P6; break;
251+
case 'f': type = Pf; break;
252+
case 'F': type = PF; break;
253+
default: return std::nullopt;
254+
}
255+
header.remove_prefix(1);
256+
257+
//Size
258+
int width, height;
259+
if (!parse_next_header_value(header, width))
260+
return std::nullopt;
261+
if (!parse_next_header_value(header, height))
262+
return std::nullopt;
263+
264+
return PNMBasicInfo { type, width, height };
265+
}
266+
267+
268+
192269
bool
193270
PNMInput::read_file_scanline(void* data, int y)
194271
{
@@ -277,28 +354,15 @@ PNMInput::read_file_scanline(void* data, int y)
277354
bool
278355
PNMInput::read_file_header()
279356
{
280-
// MagicNumber
281-
if (!Strutil::parse_char(m_remaining, 'P') || m_remaining.empty())
282-
return false;
283-
switch (m_remaining.front()) {
284-
case '1': m_pnm_type = P1; break;
285-
case '2': m_pnm_type = P2; break;
286-
case '3': m_pnm_type = P3; break;
287-
case '4': m_pnm_type = P4; break;
288-
case '5': m_pnm_type = P5; break;
289-
case '6': m_pnm_type = P6; break;
290-
case 'f': m_pnm_type = Pf; break;
291-
case 'F': m_pnm_type = PF; break;
292-
default: return false;
293-
}
294-
m_remaining.remove_prefix(1);
295-
296-
//Size
297357
int width, height;
298-
if (!nextVal(width))
299-
return false;
300-
if (!nextVal(height))
358+
359+
if (auto basic_info = read_type_and_resolution(m_remaining)) {
360+
m_pnm_type = basic_info->type;
361+
width = basic_info->width;
362+
height = basic_info->height;
363+
} else {
301364
return false;
365+
}
302366

303367
if (m_pnm_type != PF && m_pnm_type != Pf) {
304368
// Max Val
@@ -312,7 +376,6 @@ PNMInput::read_file_header()
312376
if (!(m_remaining.size() && Strutil::isspace(m_remaining.front())))
313377
return false;
314378
m_remaining.remove_prefix(1);
315-
m_after_header = m_remaining;
316379

317380
m_spec = ImageSpec(width, height,
318381
(m_pnm_type == P3 || m_pnm_type == P6) ? 3 : 1,
@@ -332,7 +395,6 @@ PNMInput::read_file_header()
332395
if (!(m_remaining.size() && Strutil::isspace(m_remaining.front())))
333396
return false;
334397
m_remaining.remove_prefix(1);
335-
m_after_header = m_remaining;
336398

337399
m_spec = ImageSpec(width, height, m_pnm_type == PF ? 3 : 1,
338400
TypeDesc::FLOAT);
@@ -345,6 +407,79 @@ PNMInput::read_file_header()
345407

346408

347409

410+
// Read only enough of the file to contain the header (at momst 1KB) into buffer
411+
string_view
412+
PNMInput::read_header_to_buffer(std::vector<char>& buffer,
413+
Filesystem::IOProxy* io)
414+
{
415+
imagesize_t header_size = std::min(static_cast<imagesize_t>(io->size()),
416+
max_pnm_header_size);
417+
buffer.resize(header_size);
418+
io->pread(buffer.data(), header_size, 0);
419+
return string_view(buffer.data(), buffer.size());
420+
}
421+
422+
423+
424+
// buffer contains at most the first 1K of the file. At this point, we know
425+
// the file seems valid. Read the rest in, appending to what we have, and
426+
// return the adjusted string_view of the contents.
427+
string_view
428+
PNMInput::append_remainder_to_buffer(std::vector<char>& buffer,
429+
Filesystem::IOProxy* io,
430+
string_view remaining)
431+
{
432+
// Assume we've already read the header into buffer
433+
imagesize_t header_size = buffer.size();
434+
imagesize_t full_size = std::min(static_cast<imagesize_t>(io->size()),
435+
max_pnm_file_size);
436+
ptrdiff_t remaining_offset = remaining.data() - buffer.data();
437+
438+
buffer.resize(full_size);
439+
io->pread(buffer.data() + header_size, full_size - header_size,
440+
header_size);
441+
442+
string_view result { buffer.data(), buffer.size() };
443+
result.remove_prefix(remaining_offset);
444+
return result;
445+
}
446+
447+
448+
449+
bool
450+
PNMInput::valid_file(Filesystem::IOProxy* ioproxy) const
451+
{
452+
DBG std::cout << "PNMInput::valid_file()\n";
453+
454+
if (!ioproxy || ioproxy->mode() != Filesystem::IOProxy::Mode::Read)
455+
return false;
456+
457+
std::vector<char> buffer;
458+
string_view header = read_header_to_buffer(buffer, ioproxy);
459+
460+
int width, height;
461+
if (auto basic_info = read_type_and_resolution(header)) {
462+
width = basic_info->width;
463+
height = basic_info->height;
464+
} else {
465+
return false;
466+
}
467+
468+
// Per spec, width and height must both be positive integers.
469+
// No formal upper limit is placed on width/height, but for sanity,
470+
// assume dimensions should be no greater than 2^12
471+
if (width < 0 || 4096 < width)
472+
return false;
473+
if (height < 0 || 4096 < height)
474+
return false;
475+
476+
DBG std::cout << "PNMInput::valid_file returned true\n";
477+
478+
return true;
479+
}
480+
481+
482+
348483
bool
349484
PNMInput::open(const std::string& name, ImageSpec& newspec,
350485
const ImageSpec& config)
@@ -371,18 +506,20 @@ PNMInput::open(const std::string& name, ImageSpec& newspec)
371506

372507
// Read the whole file's contents into m_file_contents
373508
Filesystem::IOProxy* m_io = ioproxy();
374-
m_file_contents.resize(m_io->size());
375-
m_io->pread(m_file_contents.data(), m_file_contents.size(), 0);
376-
m_remaining = string_view(m_file_contents.data(), m_file_contents.size());
377-
m_pfm_flip = false;
509+
m_remaining = read_header_to_buffer(m_file_contents, m_io);
510+
m_pfm_flip = false;
378511

379512
if (!read_file_header())
380513
return false;
381514

382515
if (!check_open(m_spec)) // check for apparently invalid values
383516
return false;
384517

385-
newspec = m_spec;
518+
m_remaining = append_remainder_to_buffer(m_file_contents, m_io,
519+
m_remaining);
520+
m_after_header = m_remaining;
521+
newspec = m_spec;
522+
386523
return true;
387524
}
388525

@@ -412,4 +549,5 @@ PNMInput::read_native_scanline(int subimage, int miplevel, int y, int z,
412549
return true;
413550
}
414551

552+
415553
OIIO_PLUGIN_NAMESPACE_END

0 commit comments

Comments
 (0)