kanidmd_lib/valueset/image/
png.rs1use super::{ImageValidationError, MAX_IMAGE_HEIGHT, MAX_IMAGE_WIDTH};
2use crate::prelude::*;
3static PNG_PRELUDE: &[u8] = &[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
4static PNG_CHUNK_IHDR: &[u8; 4] = b"IHDR";
5static PNG_CHUNK_END: &[u8; 4] = b"IEND";
6
7#[derive(Debug)]
8enum PngChunkStatus {
11 SeenEnd { has_trailer: bool },
12 MoreChunks,
13}
14
15fn png_split_chunk(buf: &[u8]) -> Result<(&[u8], &[u8], &[u8]), ImageValidationError> {
16 if buf.len() < 12 {
18 return Err(ImageValidationError::InvalidImage(format!(
19 "PNG file is too short to be valid, got {} bytes",
20 buf.len()
21 )));
22 } else {
23 #[cfg(any(debug_assertions, test))]
24 trace!("input buflen: {}", buf.len());
25 }
26 let (length_bytes, buf) = buf.split_at(4);
27 let (chunk_type, buf) = buf.split_at(4);
28
29 let length = u32::from_be_bytes(
31 length_bytes
32 .try_into()
33 .map_err(|_| ImageValidationError::InvalidImage("PNG corrupt!".to_string()))?,
34 );
35 #[cfg(any(debug_assertions, test))]
36 trace!(
37 "length_bytes: {:?} length: {} chunk_type: {:?} buflen: {}",
38 length_bytes,
39 &length,
40 &chunk_type,
41 &buf.len()
42 );
43
44 if buf.len() < (length as usize).saturating_add(4) {
45 return Err(ImageValidationError::InvalidImage(format!(
46 "PNG file is too short to be valid, failed to split at the chunk length {}, had {} bytes",
47 length,
48 buf.len(),
49 )));
50 }
51 let (chunk_data, buf) = buf.split_at(length as usize);
52 #[cfg(any(debug_assertions, test))]
53 trace!("new buflen: {}", &buf.len());
54
55 let (_checksum, buf) = buf.split_at(4);
56 #[cfg(any(debug_assertions, test))]
57 trace!("post-checksum buflen: {}", &buf.len());
58
59 Ok((chunk_type, chunk_data, buf))
60}
61
62fn png_validate_ihdr(chunk_type: &[u8], chunk_data: &[u8]) -> Result<(), ImageValidationError> {
63 if chunk_type != PNG_CHUNK_IHDR {
64 return Err(ImageValidationError::InvalidImage(
65 "PNG first chunk must be IHDR".to_string(),
66 ));
67 }
68
69 if chunk_data.len() != 13 {
70 return Err(ImageValidationError::InvalidImage(format!(
71 "PNG IHDR chunk must be 13 bytes, got {} bytes",
72 chunk_data.len()
73 )));
74 }
75
76 let (width_chunk, chunk_data) = chunk_data.split_at(4);
77 let (height_chunk, _rest) = chunk_data.split_at(4);
78 let width = u32::from_be_bytes(
79 width_chunk
80 .try_into()
81 .map_err(|_| ImageValidationError::InvalidImage("PNG corrupt!".to_string()))?,
82 );
83 let height = u32::from_be_bytes(
84 height_chunk
85 .try_into()
86 .map_err(|_| ImageValidationError::InvalidImage("PNG corrupt!".to_string()))?,
87 );
88
89 if width == 0 || height == 0 {
90 return Err(ImageValidationError::InvalidImage(
91 "PNG IHDR dimensions must be non-zero".to_string(),
92 ));
93 }
94
95 if width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT {
96 return Err(ImageValidationError::ExceedsMaxDimensions);
97 }
98
99 Ok(())
100}
101
102fn png_consume_chunks_until_iend(
104 buf: &[u8],
105) -> Result<(PngChunkStatus, &[u8]), ImageValidationError> {
106 let (chunk_type, chunk_data, buf) = png_split_chunk(buf)?;
107
108 if chunk_type == PNG_CHUNK_END {
109 if !chunk_data.is_empty() {
110 return Err(ImageValidationError::InvalidImage(
111 "PNG IEND chunk must be empty".to_string(),
112 ));
113 }
114
115 if buf.is_empty() {
116 Ok((PngChunkStatus::SeenEnd { has_trailer: false }, buf))
117 } else {
118 Ok((PngChunkStatus::SeenEnd { has_trailer: true }, buf))
119 }
120 } else {
121 Ok((PngChunkStatus::MoreChunks, buf))
122 }
123}
124
125pub fn png_has_trailer(contents: &Vec<u8>) -> Result<bool, ImageValidationError> {
127 if contents.len() < PNG_PRELUDE.len() {
128 return Err(ImageValidationError::InvalidImage(format!(
129 "PNG file is too short to be valid, got {} bytes",
130 contents.len()
131 )));
132 }
133
134 let (magic, mut buf) = contents.as_slice().split_at(PNG_PRELUDE.len());
135 if magic != PNG_PRELUDE {
136 return Err(ImageValidationError::InvalidPngPrelude);
137 }
138
139 let (chunk_type, chunk_data, new_buf) = png_split_chunk(buf)?;
140 png_validate_ihdr(chunk_type, chunk_data)?;
141 buf = new_buf;
142
143 loop {
144 let (status, new_buf) = png_consume_chunks_until_iend(buf)?;
145 buf = match status {
146 PngChunkStatus::SeenEnd { has_trailer } => return Ok(has_trailer),
147 PngChunkStatus::MoreChunks => new_buf,
148 };
149 }
150}
151
152pub fn png_lodepng_validate(
154 contents: &Vec<u8>,
155 filename: &str,
156) -> Result<(), ImageValidationError> {
157 match lodepng::decode32(contents) {
158 Ok(val) => {
159 if val.width > MAX_IMAGE_WIDTH as usize {
160 admin_debug!(
161 "PNG validation failed for {} {}",
162 filename,
163 ImageValidationError::ExceedsMaxWidth
164 );
165 Err(ImageValidationError::ExceedsMaxWidth)
166 } else if val.height > MAX_IMAGE_HEIGHT as usize {
167 admin_debug!(
168 "PNG validation failed for {} {}",
169 filename,
170 ImageValidationError::ExceedsMaxHeight
171 );
172 Err(ImageValidationError::ExceedsMaxHeight)
173 } else {
174 Ok(())
175 }
176 }
177 Err(err) => {
178 Err(ImageValidationError::InvalidImage(format!("{err:?}")))
180 }
181 }
182}
183
184#[test]
185fn test_png_consume_chunks_until_iend() {
187 let mut testchunks = vec![0, 0, 0, 0]; testchunks.extend(PNG_CHUNK_END); testchunks.extend([0, 0, 0, 1]); let expected: [u8; 0] = [];
192 let testchunks_slice = testchunks.as_slice();
193 let res = png_consume_chunks_until_iend(testchunks_slice);
194
195 match res {
197 Ok((result, buf)) => {
198 if let PngChunkStatus::MoreChunks = result {
199 panic!("Shouldn't have more chunks!");
200 }
201 assert_eq!(buf, &expected);
202 }
203 Err(err) => panic!("Error: {err:?}"),
204 };
205
206 let mut x = 10;
208 while x > 0 {
209 let newslice = &testchunks_slice[0..=x];
210 let res = png_consume_chunks_until_iend(newslice);
211 trace!("chunkstatus at size {} {:?}", x, &res);
212 assert!(res.is_err());
213 x -= 1;
214 }
215}
216
217#[test]
218fn test_png_too_short() {
219 let too_short = vec![0, 0, 0, 1, 2, 3];
220 let res = png_consume_chunks_until_iend(too_short.as_slice());
221 assert!(res.is_err());
222}
223
224#[test]
225fn audit_png_short_input_err() {
226 let short = vec![0x89u8, 0x50, 0x4e, 0x47];
227 assert!(png_has_trailer(&short).is_err());
228}
229
230#[test]
231fn audit_png_chunk_length_overflow_errs() {
232 let mut data = vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
233 data.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFD]);
234 data.extend_from_slice(PNG_CHUNK_IHDR);
235 data.extend_from_slice(&[0u8; 8]);
236 assert!(png_has_trailer(&data).is_err());
237}
238
239#[cfg(test)]
240fn png_test_chunk(chunk_type: [u8; 4], data: &[u8]) -> Vec<u8> {
241 let mut chunk = Vec::new();
242 chunk.extend_from_slice(&(data.len() as u32).to_be_bytes());
243 chunk.extend_from_slice(&chunk_type);
244 chunk.extend_from_slice(data);
245 chunk.extend_from_slice(&[0u8; 4]);
246 chunk
247}
248
249#[cfg(test)]
250fn png_test_image_with_ihdr(width: u32, height: u32) -> Vec<u8> {
251 let mut ihdr = Vec::new();
252 ihdr.extend_from_slice(&width.to_be_bytes());
253 ihdr.extend_from_slice(&height.to_be_bytes());
254 ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
255
256 let mut data = PNG_PRELUDE.to_vec();
257 data.extend_from_slice(&png_test_chunk(*PNG_CHUNK_IHDR, &ihdr));
258 data.extend_from_slice(&png_test_chunk(*PNG_CHUNK_END, &[]));
259 data
260}
261
262#[test]
263fn audit_png_signature_without_chunks_errs() {
264 let data = PNG_PRELUDE.to_vec();
265 assert!(png_has_trailer(&data).is_err());
266}
267
268#[test]
269fn audit_png_valid_file_with_trailing_bytes_has_trailer() {
270 let mut data = include_bytes!("test_images/ok.png").to_vec();
271 data.push(0);
272 assert!(png_has_trailer(&data).expect("valid PNG with trailer should parse"));
273}
274
275#[test]
276fn audit_png_nonzero_length_iend_errs() {
277 let mut data = png_test_image_with_ihdr(1, 1);
278 data.truncate(data.len() - 12);
279 data.extend_from_slice(&png_test_chunk(*PNG_CHUNK_END, &[1]));
280 assert!(png_has_trailer(&data).is_err());
281}
282
283#[test]
284fn audit_png_first_chunk_must_be_ihdr() {
285 let mut data = PNG_PRELUDE.to_vec();
286 data.extend_from_slice(&png_test_chunk(*b"tEXt", &[]));
287 data.extend_from_slice(&png_test_chunk(*PNG_CHUNK_END, &[]));
288 assert!(png_has_trailer(&data).is_err());
289}
290
291#[test]
292fn audit_png_ihdr_length_must_be_13() {
293 let mut data = PNG_PRELUDE.to_vec();
294 data.extend_from_slice(&png_test_chunk(*PNG_CHUNK_IHDR, &[]));
295 data.extend_from_slice(&png_test_chunk(*PNG_CHUNK_END, &[]));
296 assert!(png_has_trailer(&data).is_err());
297}
298
299#[test]
300fn audit_png_ihdr_zero_width_or_height_errs() {
301 let zero_width = png_test_image_with_ihdr(0, 1);
302 let zero_height = png_test_image_with_ihdr(1, 0);
303 assert!(png_has_trailer(&zero_width).is_err());
304 assert!(png_has_trailer(&zero_height).is_err());
305}
306
307#[test]
308fn audit_png_ihdr_over_limit_dimensions_err() {
309 let too_wide = png_test_image_with_ihdr(MAX_IMAGE_WIDTH + 1, 1);
310 let too_tall = png_test_image_with_ihdr(1, MAX_IMAGE_HEIGHT + 1);
311 assert!(png_has_trailer(&too_wide).is_err());
312 assert!(png_has_trailer(&too_tall).is_err());
313}