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