kanidmd_lib/valueset/image/
mod.rs1use crate::be::dbvalue::DbValueImage;
2use crate::prelude::*;
3use crate::schema::SchemaAttribute;
4use crate::valueset::ScimResolveStatus;
5use crate::valueset::{DbValueSetV2, ValueSet};
6use crypto_glue::{s256::Sha256, traits::Digest};
7use hashbrown::HashSet;
8use image::codecs::gif::GifDecoder;
9use image::codecs::webp::WebPDecoder;
10use image::ImageDecoder;
11use kanidm_proto::internal::{ImageType, ImageValue};
12use std::fmt::Display;
13use std::io::Cursor;
14
15#[derive(Debug, Clone)]
16pub struct ValueSetImage {
17 set: HashSet<ImageValue>,
18}
19
20pub(crate) const MAX_IMAGE_HEIGHT: u32 = 1024;
21pub(crate) const MAX_IMAGE_WIDTH: u32 = 1024;
22pub(crate) const MAX_FILE_SIZE: u32 = 1024 * 256;
23
24const WEBP_MAGIC: &[u8; 4] = b"RIFF";
25
26pub mod jpg;
27pub mod png;
28
29pub trait ImageValueThings {
30 fn validate_image(&self) -> Result<(), ImageValidationError>;
31 fn validate_is_png(&self) -> Result<(), ImageValidationError>;
32 fn validate_is_gif(&self) -> Result<(), ImageValidationError>;
33 fn validate_is_jpg(&self) -> Result<(), ImageValidationError>;
34 fn validate_is_webp(&self) -> Result<(), ImageValidationError>;
35 fn validate_is_svg(&self) -> Result<(), ImageValidationError>;
36
37 fn hash_imagevalue(&self) -> String;
39
40 fn get_limits(&self) -> image::Limits {
41 let mut limits = image::Limits::default();
42 limits.max_image_height = Some(MAX_IMAGE_HEIGHT);
43 limits.max_image_width = Some(MAX_IMAGE_WIDTH);
44 limits
45 }
46}
47
48#[derive(Debug)]
49pub enum ImageValidationError {
50 Acropalypse(String),
51 ExceedsMaxWidth,
52 ExceedsMaxHeight,
53 ExceedsMaxDimensions,
54 ExceedsMaxFileSize,
55 InvalidImage(String),
56 InvalidPngPrelude,
57}
58
59impl Display for ImageValidationError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 ImageValidationError::ExceedsMaxWidth => {
63 f.write_fmt(format_args!("Exceeds the maximum width: {MAX_IMAGE_WIDTH}"))
64 }
65 ImageValidationError::ExceedsMaxHeight => f.write_fmt(format_args!(
66 "Exceeds the maximum height: {MAX_IMAGE_HEIGHT}"
67 )),
68 ImageValidationError::ExceedsMaxFileSize => {
69 f.write_fmt(format_args!("Exceeds maximum file size of {MAX_FILE_SIZE}"))
70 }
71 ImageValidationError::InvalidImage(message) => {
72 if !message.is_empty() {
73 f.write_fmt(format_args!("Invalid Image: {message}"))
74 } else {
75 f.write_str("Invalid Image")
76 }
77 }
78 ImageValidationError::ExceedsMaxDimensions => f.write_fmt(format_args!(
79 "Image exceeds max dimensions of {MAX_IMAGE_WIDTH}x{MAX_IMAGE_HEIGHT}"
80 )),
81 ImageValidationError::Acropalypse(message) => {
82 if !message.is_empty() {
83 f.write_fmt(format_args!(
84 "Image has extra data, is vulnerable to Acropalypse: {message}"
85 ))
86 } else {
87 f.write_str("Image has extra data, is vulnerable to Acropalypse")
88 }
89 }
90 ImageValidationError::InvalidPngPrelude => {
91 f.write_str("Image has an invalid PNG prelude and is likely corrupt.")
92 }
93 }
94 }
95}
96
97impl ImageValueThings for ImageValue {
98 fn validate_image(&self) -> Result<(), ImageValidationError> {
99 if self.contents.len() > MAX_FILE_SIZE as usize {
100 return Err(ImageValidationError::ExceedsMaxFileSize);
101 }
102
103 match self.filetype {
104 ImageType::Gif => self.validate_is_gif(),
105 ImageType::Png => self.validate_is_png(),
106 ImageType::Svg => self.validate_is_svg(),
107 ImageType::Jpg => self.validate_is_jpg(),
108 ImageType::Webp => self.validate_is_webp(),
109 }
110 }
111
112 fn validate_is_png(&self) -> Result<(), ImageValidationError> {
114 if png::png_has_trailer(&self.contents)? {
118 return Err(ImageValidationError::Acropalypse(
119 "PNG file has a trailer which likely indicates the acropalypse vulnerability!"
120 .to_string(),
121 ));
122 }
123
124 png::png_lodepng_validate(&self.contents, &self.filename)
125 }
126
127 fn validate_is_jpg(&self) -> Result<(), ImageValidationError> {
129 jpg::check_jpg_header(&self.contents)?;
131
132 jpg::validate_decoding(&self.filename, &self.contents, self.get_limits())?;
133
134 if jpg::has_trailer(&self.contents)? {
135 Err(ImageValidationError::Acropalypse(
136 "File has a trailer which likely indicates the acropalypse vulnerability!"
137 .to_string(),
138 ))
139 } else {
140 Ok(())
141 }
142 }
143
144 fn validate_is_gif(&self) -> Result<(), ImageValidationError> {
146 let Ok(mut decoder) = GifDecoder::new(Cursor::new(&self.contents[..])) else {
147 return Err(ImageValidationError::InvalidImage(
148 "Failed to parse GIF".to_string(),
149 ));
150 };
151 let limit_result = decoder.set_limits(self.get_limits());
152 if limit_result.is_err() {
153 Err(ImageValidationError::ExceedsMaxDimensions)
154 } else {
155 Ok(())
156 }
157 }
158
159 fn validate_is_svg(&self) -> Result<(), ImageValidationError> {
161 let svg_string = std::str::from_utf8(&self.contents).map_err(|e| {
163 ImageValidationError::InvalidImage(format!(
164 "Failed to parse SVG {} as a unicode string: {:?}",
165 self.hash_imagevalue(),
166 e
167 ))
168 })?;
169 svg::read(svg_string).map_err(|e| {
170 ImageValidationError::InvalidImage(format!(
171 "Failed to parse {} as SVG: {:?}",
172 self.hash_imagevalue(),
173 e
174 ))
175 })?;
176 Ok(())
177 }
178
179 fn validate_is_webp(&self) -> Result<(), ImageValidationError> {
181 if !self.contents.starts_with(WEBP_MAGIC) {
182 return Err(ImageValidationError::InvalidImage(
183 "Failed to parse WebP file, invalid magic bytes".to_string(),
184 ));
185 }
186
187 let Ok(mut decoder) = WebPDecoder::new(Cursor::new(&self.contents[..])) else {
188 return Err(ImageValidationError::InvalidImage(
189 "Failed to parse WebP file".to_string(),
190 ));
191 };
192 match decoder.set_limits(self.get_limits()) {
193 Err(err) => {
194 sketching::admin_warn!(
195 "Image validation failed while validating {}: {:?}",
196 self.filename,
197 err
198 );
199 Err(ImageValidationError::ExceedsMaxDimensions)
200 }
201 Ok(_) => Ok(()),
202 }
203 }
204
205 fn hash_imagevalue(&self) -> String {
208 let filetype_repr = [self.filetype.clone() as u8];
209 let mut hasher = Sha256::new();
210 hasher.update(self.filename.as_bytes());
211 hasher.update(filetype_repr);
212 hasher.update(&self.contents);
213 hex::encode(hasher.finalize())
214 }
215}
216
217impl ValueSetImage {
218 pub fn new(image: ImageValue) -> Box<Self> {
219 let mut set = HashSet::new();
220 match image.validate_image() {
221 Ok(_) => {
222 set.insert(image);
223 }
224 Err(err) => {
225 admin_error!(
226 "Image {} didn't pass validation, not adding to value! Error: {:?}",
227 image.filename,
228 err
229 );
230 }
231 };
232 Box::new(ValueSetImage { set })
233 }
234
235 pub fn push(&mut self, image: ImageValue) -> bool {
237 match image.validate_image() {
238 Ok(_) => self.set.insert(image),
239 Err(err) => {
240 admin_error!(
241 "Image didn't pass validation, not adding to value! Error: {}",
242 err
243 );
244 false
245 }
246 }
247 }
248
249 pub fn from_dbvs2(data: &[DbValueImage]) -> Result<ValueSet, OperationError> {
250 Ok(Box::new(ValueSetImage {
251 set: data
252 .iter()
253 .cloned()
254 .map(|e| match e {
255 DbValueImage::V1 {
256 filename,
257 filetype,
258 contents,
259 } => ImageValue::new(filename, filetype, contents),
260 })
261 .collect(),
262 }))
263 }
264
265 #[allow(clippy::should_implement_trait)]
268 pub fn from_iter<T>(iter: T) -> Option<Box<ValueSetImage>>
269 where
270 T: IntoIterator<Item = ImageValue>,
271 {
272 let mut set: HashSet<ImageValue> = HashSet::new();
273 for image in iter {
274 match image.validate_image() {
275 Ok(_) => set.insert(image),
276 Err(err) => {
277 admin_error!(
278 "Image didn't pass validation, not adding to value! Error: {}",
279 err
280 );
281 return None;
282 }
283 };
284 }
285 Some(Box::new(ValueSetImage { set }))
286 }
287}
288
289impl ValueSetT for ValueSetImage {
290 fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
291 match value {
292 Value::Image(image) => match self.set.contains(&image) {
293 true => Ok(false), false => Ok(self.push(image)), },
296 _ => {
297 debug_assert!(false);
298 Err(OperationError::InvalidValueState)
299 }
300 }
301 }
302
303 fn clear(&mut self) {
304 self.set.clear();
305 }
306
307 fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
308 match pv {
309 PartialValue::Image(pv) => {
310 let imgset = self.set.clone();
311
312 let res: Vec<bool> = imgset
313 .iter()
314 .filter(|image| &image.hash_imagevalue() == pv)
315 .map(|image| self.set.remove(image))
316 .collect();
317 res.into_iter().any(|e| e)
318 }
319 _ => {
320 debug_assert!(false);
321 false
322 }
323 }
324 }
325
326 fn contains(&self, pv: &PartialValue) -> bool {
327 match pv {
328 PartialValue::Image(pvhash) => {
329 if let Some(image) = self.set.iter().take(1).next() {
330 &image.hash_imagevalue() == pvhash
331 } else {
332 false
333 }
334 }
335 _ => false,
336 }
337 }
338
339 fn substring(&self, _pv: &PartialValue) -> bool {
340 false
341 }
342
343 fn startswith(&self, _pv: &PartialValue) -> bool {
344 false
345 }
346
347 fn endswith(&self, _pv: &PartialValue) -> bool {
348 false
349 }
350
351 fn lessthan(&self, _pv: &PartialValue) -> bool {
352 false
353 }
354
355 fn len(&self) -> usize {
356 self.set.len()
357 }
358
359 fn generate_idx_eq_keys(&self) -> Vec<String> {
360 self.set
361 .iter()
362 .map(|image| image.hash_imagevalue())
363 .collect()
364 }
365
366 fn syntax(&self) -> SyntaxType {
367 SyntaxType::Image
368 }
369
370 fn validate(&self, schema_attr: &SchemaAttribute) -> bool {
371 if !schema_attr.multivalue && self.set.len() > 1 {
372 return false;
373 }
374 self.set.iter().all(|image| {
375 image
376 .validate_image()
377 .map_err(|err| error!("Image {} failed validation: {}", image.filename, err))
378 .is_ok()
379 })
380 }
381
382 fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
383 Box::new(self.set.iter().map(|image| image.hash_imagevalue()))
384 }
385
386 fn to_scim_value(&self) -> Option<ScimResolveStatus> {
387 Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
397 self.set
398 .iter()
399 .map(|image| image.hash_imagevalue())
400 .collect::<Vec<_>>(),
401 )))
402 }
403
404 fn to_db_valueset_v2(&self) -> DbValueSetV2 {
405 DbValueSetV2::Image(
406 self.set
407 .iter()
408 .cloned()
409 .map(|e| crate::be::dbvalue::DbValueImage::V1 {
410 filename: e.filename,
411 filetype: e.filetype,
412 contents: e.contents,
413 })
414 .collect(),
415 )
416 }
417
418 fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
419 Box::new(
420 self.set
421 .iter()
422 .map(|image| PartialValue::Image(image.hash_imagevalue())),
423 )
424 }
425
426 fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
427 Box::new(self.set.iter().cloned().map(Value::Image))
428 }
429
430 fn equal(&self, other: &ValueSet) -> bool {
431 if let Some(other) = other.as_imageset() {
432 &self.set == other
433 } else {
434 debug_assert!(false);
435 false
436 }
437 }
438
439 fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
440 if let Some(b) = other.as_imageset() {
441 mergesets!(self.set, b)
442 } else {
443 debug_assert!(false);
444 Err(OperationError::InvalidValueState)
445 }
446 }
447
448 fn as_imageset(&self) -> Option<&HashSet<ImageValue>> {
449 Some(&self.set)
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::{ImageType, ImageValue, ImageValueThings};
457
458 #[test]
459 fn test_imagevalue_loading() {
461 ["gif", "png", "jpg", "webp"]
462 .into_iter()
463 .for_each(|extension| {
464 let filename = format!(
466 "{}/src/valueset/image/test_images/oversize_dimensions.{extension}",
467 env!("CARGO_MANIFEST_DIR")
468 );
469 trace!("testing {}", &filename);
470 let image = ImageValue {
471 filename: format!("oversize_dimensions.{extension}"),
472 filetype: ImageType::try_from(extension).unwrap(),
473 contents: std::fs::read(filename).unwrap(),
474 };
475 let res = image.validate_image();
476 trace!("{:?}", &res);
477 assert!(res.is_err());
478
479 let filename = format!(
480 "{}/src/valueset/image/test_images/ok.svg",
481 env!("CARGO_MANIFEST_DIR")
482 );
483 let image = ImageValue {
484 filename: filename.clone(),
485 filetype: ImageType::Svg,
486 contents: std::fs::read(&filename).unwrap(),
487 };
488 let res = image.validate_image();
489 trace!("SVG Validation result of {}: {:?}", filename, &res);
490 assert!(res.is_ok());
491 assert!(!image.hash_imagevalue().is_empty());
492 });
493
494 let filename = format!(
495 "{}/src/valueset/image/test_images/ok.svg",
496 env!("CARGO_MANIFEST_DIR")
497 );
498 let image = ImageValue {
499 filename: filename.clone(),
500 filetype: ImageType::Svg,
501 contents: std::fs::read(&filename).unwrap(),
502 };
503 let res = image.validate_image();
504 trace!("SVG Validation result of {}: {:?}", filename, &res);
505 assert!(res.is_ok());
506 assert!(!image.hash_imagevalue().is_empty());
507 }
508
509 }