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