1use crate::{
6 openapi::{OpenApiOperation, OpenApiSecurityRequirement, OpenApiSpec},
7 Error, Result,
8};
9use base32::Alphabet;
10use base64::{engine::general_purpose, Engine as _};
11use jsonschema::{self, Draft, Validator as JSONSchema};
12use prost_reflect::{DescriptorPool, DynamicMessage};
13use serde_json::{json, Value};
14
15#[derive(Debug)]
17pub enum Validator {
18 JsonSchema(JSONSchema),
20 OpenApi31Schema(JSONSchema, Value),
22 OpenApi(Box<OpenApiSpec>),
24 Protobuf(DescriptorPool),
26}
27
28impl Validator {
29 pub fn from_json_schema(schema: &Value) -> Result<Self> {
31 let compiled = jsonschema::options()
32 .with_draft(Draft::Draft7)
33 .build(schema)
34 .map_err(|e| Error::validation(format!("Failed to compile JSON schema: {}", e)))?;
35
36 Ok(Self::JsonSchema(compiled))
37 }
38
39 pub fn from_openapi31_schema(schema: &Value) -> Result<Self> {
41 let compiled =
42 jsonschema::options().with_draft(Draft::Draft7).build(schema).map_err(|e| {
43 Error::validation(format!("Failed to compile OpenAPI 3.1 schema: {}", e))
44 })?;
45
46 Ok(Self::OpenApi31Schema(compiled, schema.clone()))
47 }
48
49 pub fn from_openapi(spec: &Value) -> Result<Self> {
51 if let Some(openapi_version) = spec.get("openapi") {
53 if let Some(version_str) = openapi_version.as_str() {
54 if !version_str.starts_with("3.") {
55 return Err(Error::validation(format!(
56 "Unsupported OpenAPI version: {}. Only 3.x is supported",
57 version_str
58 )));
59 }
60 }
61 }
62
63 let openapi_spec = OpenApiSpec::from_json(spec.clone())
65 .map_err(|e| Error::validation(format!("Failed to parse OpenAPI spec: {}", e)))?;
66
67 Ok(Self::OpenApi(Box::new(openapi_spec)))
68 }
69
70 pub fn from_protobuf(descriptor: &[u8]) -> Result<Self> {
72 let mut pool = DescriptorPool::new();
73 pool.decode_file_descriptor_set(descriptor)
74 .map_err(|e| Error::validation(format!("Invalid protobuf descriptor: {}", e)))?;
75 Ok(Self::Protobuf(pool))
76 }
77
78 pub fn validate(&self, data: &Value) -> Result<()> {
80 match self {
81 Self::JsonSchema(schema) => {
82 let mut errors = Vec::new();
83 for error in schema.iter_errors(data) {
84 errors.push(error.to_string());
85 }
86
87 if errors.is_empty() {
88 Ok(())
89 } else {
90 Err(Error::validation(format!("Validation failed: {}", errors.join(", "))))
91 }
92 }
93 Self::OpenApi31Schema(schema, original_schema) => {
94 let mut errors = Vec::new();
96 for error in schema.iter_errors(data) {
97 errors.push(error.to_string());
98 }
99
100 if !errors.is_empty() {
101 return Err(Error::validation(format!(
102 "Validation failed: {}",
103 errors.join(", ")
104 )));
105 }
106
107 self.validate_openapi31_schema(data, original_schema)
109 }
110 Self::OpenApi(_spec) => {
111 if data.is_object() {
113 Ok(())
115 } else {
116 Err(Error::validation("OpenAPI validation expects an object".to_string()))
117 }
118 }
119 Self::Protobuf(_) => {
120 tracing::warn!("Protobuf validation requires binary data and descriptors - use validate_protobuf() functions directly");
124 Ok(())
125 }
126 }
127 }
128
129 pub fn is_implemented(&self) -> bool {
131 match self {
132 Self::JsonSchema(_) => true,
133 Self::OpenApi31Schema(_, _) => true,
134 Self::OpenApi(_) => true, Self::Protobuf(_) => true, }
137 }
138
139 pub fn validate_openapi_ext(&self, data: &Value, openapi_schema: &Value) -> Result<()> {
141 match self {
142 Self::JsonSchema(_) => {
143 self.validate_openapi31_schema(data, openapi_schema)
145 }
146 Self::OpenApi31Schema(_, _) => {
147 self.validate_openapi31_schema(data, openapi_schema)
149 }
150 Self::OpenApi(_spec) => {
151 if data.is_object() {
153 Ok(())
154 } else {
155 Err(Error::validation("OpenAPI validation expects an object".to_string()))
156 }
157 }
158 Self::Protobuf(_) => {
159 tracing::warn!("Protobuf validation requires binary data and descriptors - use validate_protobuf() functions directly");
161 Ok(())
162 }
163 }
164 }
165
166 fn validate_openapi31_schema(&self, data: &Value, schema: &Value) -> Result<()> {
168 self.validate_openapi31_constraints(data, schema, "")
169 }
170
171 fn validate_openapi31_constraints(
173 &self,
174 data: &Value,
175 schema: &Value,
176 path: &str,
177 ) -> Result<()> {
178 let schema_obj = schema
179 .as_object()
180 .ok_or_else(|| Error::validation(format!("{}: Schema must be an object", path)))?;
181
182 if let Some(type_str) = schema_obj.get("type").and_then(|v| v.as_str()) {
184 match type_str {
185 "number" | "integer" => self.validate_number_constraints(data, schema_obj, path)?,
186 "array" => self.validate_array_constraints(data, schema_obj, path)?,
187 "object" => self.validate_object_constraints(data, schema_obj, path)?,
188 "string" => self.validate_string_constraints(data, schema_obj, path)?,
189 _ => {} }
191 }
192
193 if let Some(all_of) = schema_obj.get("allOf").and_then(|v| v.as_array()) {
195 for subschema in all_of {
196 self.validate_openapi31_constraints(data, subschema, path)?;
197 }
198 }
199
200 if let Some(any_of) = schema_obj.get("anyOf").and_then(|v| v.as_array()) {
201 let mut errors = Vec::new();
202 for subschema in any_of {
203 if let Err(e) = self.validate_openapi31_constraints(data, subschema, path) {
204 errors.push(e.to_string());
205 } else {
206 return Ok(());
208 }
209 }
210 if !errors.is_empty() {
211 return Err(Error::validation(format!(
212 "{}: No subschema in anyOf matched: {}",
213 path,
214 errors.join(", ")
215 )));
216 }
217 }
218
219 if let Some(one_of) = schema_obj.get("oneOf").and_then(|v| v.as_array()) {
220 let mut matches = 0;
221 for subschema in one_of {
222 if self.validate_openapi31_constraints(data, subschema, path).is_ok() {
223 matches += 1;
224 }
225 }
226 if matches != 1 {
227 return Err(Error::validation(format!(
228 "{}: Expected exactly one subschema in oneOf to match, got {}",
229 path, matches
230 )));
231 }
232 }
233
234 if let Some(content_encoding) = schema_obj.get("contentEncoding").and_then(|v| v.as_str()) {
236 self.validate_content_encoding(data.as_str(), content_encoding, path)?;
237 }
238
239 Ok(())
240 }
241
242 fn validate_number_constraints(
244 &self,
245 data: &Value,
246 schema: &serde_json::Map<String, Value>,
247 path: &str,
248 ) -> Result<()> {
249 let num = data
250 .as_f64()
251 .ok_or_else(|| Error::validation(format!("{}: Expected number, got {}", path, data)))?;
252
253 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
255 if multiple_of > 0.0 && (num / multiple_of) % 1.0 != 0.0 {
256 return Err(Error::validation(format!(
257 "{}: {} is not a multiple of {}",
258 path, num, multiple_of
259 )));
260 }
261 }
262
263 if let Some(excl_min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
265 if num <= excl_min {
266 return Err(Error::validation(format!(
267 "{}: {} must be greater than {}",
268 path, num, excl_min
269 )));
270 }
271 }
272
273 if let Some(excl_max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
275 if num >= excl_max {
276 return Err(Error::validation(format!(
277 "{}: {} must be less than {}",
278 path, num, excl_max
279 )));
280 }
281 }
282
283 Ok(())
284 }
285
286 fn validate_array_constraints(
288 &self,
289 data: &Value,
290 schema: &serde_json::Map<String, Value>,
291 path: &str,
292 ) -> Result<()> {
293 let arr = data
294 .as_array()
295 .ok_or_else(|| Error::validation(format!("{}: Expected array, got {}", path, data)))?;
296
297 if let Some(min_items) = schema.get("minItems").and_then(|v| v.as_u64()).map(|v| v as usize)
299 {
300 if arr.len() < min_items {
301 return Err(Error::validation(format!(
302 "{}: Array has {} items, minimum is {}",
303 path,
304 arr.len(),
305 min_items
306 )));
307 }
308 }
309
310 if let Some(max_items) = schema.get("maxItems").and_then(|v| v.as_u64()).map(|v| v as usize)
312 {
313 if arr.len() > max_items {
314 return Err(Error::validation(format!(
315 "{}: Array has {} items, maximum is {}",
316 path,
317 arr.len(),
318 max_items
319 )));
320 }
321 }
322
323 if let Some(unique) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
325 if unique && !self.has_unique_items(arr) {
326 return Err(Error::validation(format!("{}: Array items must be unique", path)));
327 }
328 }
329
330 if let Some(items_schema) = schema.get("items") {
332 for (idx, item) in arr.iter().enumerate() {
333 let item_path = format!("{}[{}]", path, idx);
334 self.validate_openapi31_constraints(item, items_schema, &item_path)?;
335 }
336 }
337
338 Ok(())
339 }
340
341 fn validate_object_constraints(
343 &self,
344 data: &Value,
345 schema: &serde_json::Map<String, Value>,
346 path: &str,
347 ) -> Result<()> {
348 let obj = data
349 .as_object()
350 .ok_or_else(|| Error::validation(format!("{}: Expected object, got {}", path, data)))?;
351
352 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
354 for req_prop in required {
355 if let Some(prop_name) = req_prop.as_str() {
356 if !obj.contains_key(prop_name) {
357 return Err(Error::validation(format!(
358 "{}: Missing required property '{}'",
359 path, prop_name
360 )));
361 }
362 }
363 }
364 }
365
366 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
368 for (prop_name, prop_schema) in properties {
369 if let Some(prop_value) = obj.get(prop_name) {
370 let prop_path = format!("{}/{}", path, prop_name);
371 self.validate_openapi31_constraints(prop_value, prop_schema, &prop_path)?;
372 }
373 }
374 }
375
376 Ok(())
377 }
378
379 fn validate_string_constraints(
381 &self,
382 data: &Value,
383 schema: &serde_json::Map<String, Value>,
384 path: &str,
385 ) -> Result<()> {
386 let _str_val = data
387 .as_str()
388 .ok_or_else(|| Error::validation(format!("{}: Expected string, got {}", path, data)))?;
389
390 if schema.get("contentEncoding").is_some() {
393 }
395
396 Ok(())
397 }
398
399 fn validate_content_encoding(
401 &self,
402 data: Option<&str>,
403 encoding: &str,
404 path: &str,
405 ) -> Result<()> {
406 let str_data = data.ok_or_else(|| {
407 Error::validation(format!("{}: Content encoding requires string data", path))
408 })?;
409
410 match encoding {
411 "base64" => {
412 if general_purpose::STANDARD.decode(str_data).is_err() {
413 return Err(Error::validation(format!("{}: Invalid base64 encoding", path)));
414 }
415 }
416 "base64url" => {
417 use base64::engine::general_purpose::URL_SAFE;
418 use base64::Engine;
419 if URL_SAFE.decode(str_data).is_err() {
420 return Err(Error::validation(format!("{}: Invalid base64url encoding", path)));
421 }
422 }
423 "base32" => {
424 if base32::decode(Alphabet::Rfc4648 { padding: false }, str_data).is_none() {
425 return Err(Error::validation(format!("{}: Invalid base32 encoding", path)));
426 }
427 }
428 "hex" | "binary" => {
429 if hex::decode(str_data).is_err() {
430 return Err(Error::validation(format!(
431 "{}: Invalid {} encoding",
432 path, encoding
433 )));
434 }
435 }
436 _ => {
438 tracing::warn!(
440 "{}: Unknown content encoding '{}', skipping validation",
441 path,
442 encoding
443 );
444 }
445 }
446
447 Ok(())
448 }
449
450 fn has_unique_items(&self, arr: &[Value]) -> bool {
452 let mut seen = std::collections::HashSet::new();
453 for item in arr {
454 let item_str = serde_json::to_string(item).unwrap_or_default();
455 if !seen.insert(item_str) {
456 return false;
457 }
458 }
459 true
460 }
461}
462
463#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
465pub struct ValidationResult {
466 pub valid: bool,
468 pub errors: Vec<String>,
470 pub warnings: Vec<String>,
472}
473
474impl ValidationResult {
475 pub fn success() -> Self {
477 Self {
478 valid: true,
479 errors: Vec::new(),
480 warnings: Vec::new(),
481 }
482 }
483
484 pub fn failure(errors: Vec<String>) -> Self {
486 Self {
487 valid: false,
488 errors,
489 warnings: Vec::new(),
490 }
491 }
492
493 pub fn with_warning(mut self, warning: String) -> Self {
495 self.warnings.push(warning);
496 self
497 }
498}
499
500pub fn validate_json_schema(data: &Value, schema: &Value) -> ValidationResult {
502 match Validator::from_json_schema(schema) {
503 Ok(validator) => match validator.validate(data) {
504 Ok(_) => ValidationResult::success(),
505 Err(Error::Validation { message }) => ValidationResult::failure(vec![message]),
506 Err(e) => ValidationResult::failure(vec![format!("Unexpected error: {}", e)]),
507 },
508 Err(e) => ValidationResult::failure(vec![format!("Schema compilation error: {}", e)]),
509 }
510}
511
512pub fn validate_openapi(data: &Value, spec: &Value) -> ValidationResult {
514 let spec_obj = match spec.as_object() {
516 Some(obj) => obj,
517 None => {
518 return ValidationResult::failure(vec!["OpenAPI spec must be an object".to_string()])
519 }
520 };
521
522 let mut errors = Vec::new();
524
525 if !spec_obj.contains_key("openapi") {
526 errors.push("Missing required 'openapi' field".to_string());
527 } else if let Some(version) = spec_obj.get("openapi").and_then(|v| v.as_str()) {
528 if !version.starts_with("3.") {
529 errors.push(format!("Unsupported OpenAPI version: {}. Only 3.x is supported", version));
530 }
531 }
532
533 if !spec_obj.contains_key("info") {
534 errors.push("Missing required 'info' field".to_string());
535 } else if let Some(info) = spec_obj.get("info").and_then(|v| v.as_object()) {
536 if !info.contains_key("title") {
537 errors.push("Missing required 'info.title' field".to_string());
538 }
539 if !info.contains_key("version") {
540 errors.push("Missing required 'info.version' field".to_string());
541 }
542 }
543
544 if !spec_obj.contains_key("paths") {
545 errors.push("Missing required 'paths' field".to_string());
546 }
547
548 if !errors.is_empty() {
549 return ValidationResult::failure(errors);
550 }
551
552 if serde_json::from_value::<openapiv3::OpenAPI>(spec.clone()).is_ok() {
554 let _spec_wrapper = OpenApiSpec::from_json(spec.clone()).unwrap_or_else(|_| {
555 OpenApiSpec::from_json(json!({}))
557 .expect("Empty JSON object should always create valid OpenApiSpec")
558 });
559
560 if data.is_object() {
563 ValidationResult::success()
566 .with_warning("OpenAPI schema validation available - use validate_openapi_with_path for operation-specific validation".to_string())
567 } else {
568 ValidationResult::failure(vec![
569 "Request/response data must be a JSON object".to_string()
570 ])
571 }
572 } else {
573 ValidationResult::failure(vec!["Failed to parse OpenAPI specification".to_string()])
574 }
575}
576
577pub fn validate_openapi_operation(
579 _data: &Value,
580 spec: &OpenApiSpec,
581 path: &str,
582 method: &str,
583 _is_request: bool,
584) -> ValidationResult {
585 let mut errors = Vec::new();
586
587 if let Some(path_item_ref) = spec.spec.paths.paths.get(path) {
589 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
591 let operation = match method.to_uppercase().as_str() {
592 "GET" => path_item.get.as_ref(),
593 "POST" => path_item.post.as_ref(),
594 "PUT" => path_item.put.as_ref(),
595 "DELETE" => path_item.delete.as_ref(),
596 "PATCH" => path_item.patch.as_ref(),
597 "HEAD" => path_item.head.as_ref(),
598 "OPTIONS" => path_item.options.as_ref(),
599 _ => None,
600 };
601
602 if operation.is_some() {
603 } else {
606 errors.push(format!("Method {} not found for path {}", method, path));
607 }
608 } else {
609 errors
610 .push(format!("Path {} contains a reference, not supported for validation", path));
611 }
612 } else {
613 errors.push(format!("Path {} not found in OpenAPI spec", path));
614 }
615
616 if errors.is_empty() {
617 ValidationResult::success()
618 } else {
619 ValidationResult::failure(errors)
620 }
621}
622
623pub fn validate_protobuf(data: &[u8], descriptor_data: &[u8]) -> ValidationResult {
625 let mut pool = DescriptorPool::new();
626 if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
627 return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
628 }
629
630 let Some(message_descriptor) = pool.all_messages().next() else {
631 return ValidationResult::failure(vec![
632 "Protobuf descriptor set does not contain any message descriptors".to_string(),
633 ]);
634 };
635
636 match DynamicMessage::decode(message_descriptor, data) {
637 Ok(_) => ValidationResult::success(),
638 Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
639 }
640}
641
642pub fn validate_protobuf_message(
644 data: &[u8],
645 message_descriptor: &prost_reflect::MessageDescriptor,
646) -> Result<()> {
647 match DynamicMessage::decode(message_descriptor.clone(), data) {
649 Ok(_) => Ok(()),
650 Err(e) => Err(Error::validation(format!("Protobuf validation failed: {}", e))),
651 }
652}
653
654pub fn validate_protobuf_with_type(
656 data: &[u8],
657 descriptor_data: &[u8],
658 message_type_name: &str,
659) -> ValidationResult {
660 let mut pool = DescriptorPool::new();
661 if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
662 return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
663 }
664
665 let descriptor = pool.get_message_by_name(message_type_name).or_else(|| {
666 pool.all_messages().find(|msg| {
667 msg.name() == message_type_name || msg.full_name().ends_with(message_type_name)
668 })
669 });
670
671 let Some(message_descriptor) = descriptor else {
672 return ValidationResult::failure(vec![format!(
673 "Message type '{}' not found in descriptor set",
674 message_type_name
675 )]);
676 };
677
678 match DynamicMessage::decode(message_descriptor, data) {
679 Ok(_) => ValidationResult::success(),
680 Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
681 }
682}
683
684pub fn validate_openapi_security(
686 spec: &OpenApiSpec,
687 security_requirements: &[OpenApiSecurityRequirement],
688 auth_header: Option<&str>,
689 api_key: Option<&str>,
690) -> ValidationResult {
691 match spec.validate_security_requirements(security_requirements, auth_header, api_key) {
692 Ok(_) => ValidationResult::success(),
693 Err(e) => ValidationResult::failure(vec![format!("Security validation failed: {}", e)]),
694 }
695}
696
697pub fn validate_openapi_operation_security(
699 spec: &OpenApiSpec,
700 path: &str,
701 method: &str,
702 auth_header: Option<&str>,
703 api_key: Option<&str>,
704) -> ValidationResult {
705 let operations = spec.operations_for_path(path);
707
708 let operation = operations
710 .iter()
711 .find(|(op_method, _)| op_method.to_uppercase() == method.to_uppercase());
712
713 let operation = match operation {
714 Some((_, op)) => op,
715 None => {
716 return ValidationResult::failure(vec![format!(
717 "Operation not found: {} {}",
718 method, path
719 )])
720 }
721 };
722
723 let openapi_operation =
725 OpenApiOperation::from_operation(method, path.to_string(), operation, spec);
726
727 if let Some(ref security_reqs) = openapi_operation.security {
729 if !security_reqs.is_empty() {
730 return validate_openapi_security(spec, security_reqs, auth_header, api_key);
731 }
732 }
733
734 let global_security = spec.get_global_security_requirements();
736 if !global_security.is_empty() {
737 return validate_openapi_security(spec, &global_security, auth_header, api_key);
738 }
739
740 ValidationResult::success()
742}
743
744pub fn sanitize_html(input: &str) -> String {
762 input
763 .replace('&', "&")
764 .replace('<', "<")
765 .replace('>', ">")
766 .replace('"', """)
767 .replace('\'', "'")
768 .replace('/', "/")
769}
770
771pub fn validate_safe_path(path: &str) -> Result<String> {
791 if path.contains('\0') {
793 return Err(Error::validation("Path contains null bytes".to_string()));
794 }
795
796 if path.contains("..") {
798 return Err(Error::validation("Path traversal detected: '..' not allowed".to_string()));
799 }
800
801 if path.contains('~') {
803 return Err(Error::validation("Home directory expansion '~' not allowed".to_string()));
804 }
805
806 if path.starts_with('/') {
808 return Err(Error::validation("Absolute paths not allowed".to_string()));
809 }
810
811 if path.len() >= 2 && path.chars().nth(1) == Some(':') {
813 return Err(Error::validation("Absolute paths with drive letters not allowed".to_string()));
814 }
815
816 if path.starts_with("\\\\") || path.starts_with("//") {
818 return Err(Error::validation("UNC paths not allowed".to_string()));
819 }
820
821 let normalized = path.replace('\\', "/");
823
824 if normalized.contains("//") {
826 return Err(Error::validation("Path contains empty segments".to_string()));
827 }
828
829 Ok(normalized)
830}
831
832pub fn sanitize_sql(input: &str) -> String {
849 input.replace('\'', "''")
851}
852
853pub fn validate_command_arg(arg: &str) -> Result<String> {
876 let dangerous_chars = [
878 '|', ';', '&', '<', '>', '`', '$', '(', ')', '*', '?', '[', ']', '{', '}', '~', '!', '\n',
879 '\r', '\0',
880 ];
881
882 for ch in dangerous_chars.iter() {
883 if arg.contains(*ch) {
884 return Err(Error::validation(format!(
885 "Command argument contains dangerous character: '{}'",
886 ch
887 )));
888 }
889 }
890
891 if arg.contains("$(") {
893 return Err(Error::validation("Command substitution pattern '$(' not allowed".to_string()));
894 }
895
896 Ok(arg.to_string())
897}
898
899pub fn sanitize_json_string(input: &str) -> String {
913 input
914 .replace('\\', "\\\\") .replace('"', "\\\"")
916 .replace('\n', "\\n")
917 .replace('\r', "\\r")
918 .replace('\t', "\\t")
919}
920
921pub fn validate_url_safe(url: &str) -> Result<String> {
941 let url_lower = url.to_lowercase();
943
944 let localhost_patterns = ["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"];
946 for pattern in localhost_patterns.iter() {
947 if url_lower.contains(pattern) {
948 return Err(Error::validation(
949 "URLs pointing to localhost are not allowed".to_string(),
950 ));
951 }
952 }
953
954 let private_ranges = [
956 "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.",
957 "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.",
958 "172.31.", "192.168.",
959 ];
960 for range in private_ranges.iter() {
961 if url_lower.contains(range) {
962 return Err(Error::validation(format!(
963 "URLs pointing to private IP range '{}' are not allowed",
964 range
965 )));
966 }
967 }
968
969 if url_lower.contains("169.254.") {
971 return Err(Error::validation(
972 "URLs pointing to link-local addresses (169.254.x) are not allowed".to_string(),
973 ));
974 }
975
976 let metadata_endpoints = [
978 "metadata.google.internal",
979 "169.254.169.254", "fd00:ec2::254", ];
982 for endpoint in metadata_endpoints.iter() {
983 if url_lower.contains(endpoint) {
984 return Err(Error::validation(format!(
985 "URLs pointing to cloud metadata endpoint '{}' are not allowed",
986 endpoint
987 )));
988 }
989 }
990
991 Ok(url.to_string())
992}
993
994pub fn sanitize_header_value(input: &str) -> String {
1009 input.replace(['\r', '\n'], "").trim().to_string()
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 #[test]
1018 fn test_validation_result_success() {
1019 let result = ValidationResult::success();
1020 assert!(result.valid);
1021 assert!(result.errors.is_empty());
1022 assert!(result.warnings.is_empty());
1023 }
1024
1025 #[test]
1026 fn test_validation_result_failure() {
1027 let errors = vec!["error1".to_string(), "error2".to_string()];
1028 let result = ValidationResult::failure(errors.clone());
1029 assert!(!result.valid);
1030 assert_eq!(result.errors, errors);
1031 assert!(result.warnings.is_empty());
1032 }
1033
1034 #[test]
1035 fn test_validation_result_with_warning() {
1036 let result = ValidationResult::success()
1037 .with_warning("warning1".to_string())
1038 .with_warning("warning2".to_string());
1039 assert!(result.valid);
1040 assert_eq!(result.warnings.len(), 2);
1041 }
1042
1043 #[test]
1044 fn test_validator_from_json_schema() {
1045 let schema = json!({
1046 "type": "object",
1047 "properties": {
1048 "name": {"type": "string"}
1049 }
1050 });
1051
1052 let validator = Validator::from_json_schema(&schema);
1053 assert!(validator.is_ok());
1054 assert!(validator.unwrap().is_implemented());
1055 }
1056
1057 #[test]
1058 fn test_validator_from_json_schema_invalid() {
1059 let schema = json!({
1060 "type": "invalid_type"
1061 });
1062
1063 let validator = Validator::from_json_schema(&schema);
1065 assert!(validator.is_err());
1066 }
1067
1068 #[test]
1069 fn test_validator_validate_json_schema_success() {
1070 let schema = json!({
1071 "type": "object",
1072 "properties": {
1073 "name": {"type": "string"}
1074 }
1075 });
1076
1077 let validator = Validator::from_json_schema(&schema).unwrap();
1078 let data = json!({"name": "test"});
1079
1080 assert!(validator.validate(&data).is_ok());
1081 }
1082
1083 #[test]
1084 fn test_validator_validate_json_schema_failure() {
1085 let schema = json!({
1086 "type": "object",
1087 "properties": {
1088 "name": {"type": "string"}
1089 }
1090 });
1091
1092 let validator = Validator::from_json_schema(&schema).unwrap();
1093 let data = json!({"name": 123});
1094
1095 assert!(validator.validate(&data).is_err());
1096 }
1097
1098 #[test]
1099 fn test_validator_from_openapi() {
1100 let spec = json!({
1101 "openapi": "3.0.0",
1102 "info": {"title": "Test", "version": "1.0.0"},
1103 "paths": {}
1104 });
1105
1106 let validator = Validator::from_openapi(&spec);
1107 assert!(validator.is_ok());
1108 }
1109
1110 #[test]
1111 fn test_validator_from_openapi_unsupported_version() {
1112 let spec = json!({
1113 "openapi": "2.0.0",
1114 "info": {"title": "Test", "version": "1.0.0"},
1115 "paths": {}
1116 });
1117
1118 let validator = Validator::from_openapi(&spec);
1119 assert!(validator.is_err());
1120 }
1121
1122 #[test]
1123 fn test_validator_validate_openapi() {
1124 let spec = json!({
1125 "openapi": "3.0.0",
1126 "info": {"title": "Test", "version": "1.0.0"},
1127 "paths": {}
1128 });
1129
1130 let validator = Validator::from_openapi(&spec).unwrap();
1131 let data = json!({"key": "value"});
1132
1133 assert!(validator.validate(&data).is_ok());
1134 }
1135
1136 #[test]
1137 fn test_validator_validate_openapi_non_object() {
1138 let spec = json!({
1139 "openapi": "3.0.0",
1140 "info": {"title": "Test", "version": "1.0.0"},
1141 "paths": {}
1142 });
1143
1144 let validator = Validator::from_openapi(&spec).unwrap();
1145 let data = json!("string");
1146
1147 assert!(validator.validate(&data).is_err());
1148 }
1149
1150 #[test]
1151 fn test_validate_json_schema_function() {
1152 let schema = json!({
1153 "type": "object",
1154 "properties": {
1155 "age": {"type": "number"}
1156 }
1157 });
1158
1159 let data = json!({"age": 25});
1160 let result = validate_json_schema(&data, &schema);
1161 assert!(result.valid);
1162
1163 let data = json!({"age": "25"});
1164 let result = validate_json_schema(&data, &schema);
1165 assert!(!result.valid);
1166 }
1167
1168 #[test]
1169 fn test_validate_openapi_function() {
1170 let spec = json!({
1171 "openapi": "3.0.0",
1172 "info": {"title": "Test", "version": "1.0.0"},
1173 "paths": {}
1174 });
1175
1176 let data = json!({"test": "value"});
1177 let result = validate_openapi(&data, &spec);
1178 assert!(result.valid);
1179 }
1180
1181 #[test]
1182 fn test_validate_openapi_missing_fields() {
1183 let spec = json!({
1184 "openapi": "3.0.0"
1185 });
1186
1187 let data = json!({});
1188 let result = validate_openapi(&data, &spec);
1189 assert!(!result.valid);
1190 assert!(!result.errors.is_empty());
1191 }
1192
1193 #[test]
1194 fn test_validate_number_constraints_multiple_of() {
1195 let schema = json!({
1196 "type": "number",
1197 "multipleOf": 5.0
1198 });
1199
1200 let validator = Validator::from_json_schema(&schema).unwrap();
1201
1202 let data = json!(10);
1203 assert!(validator.validate(&data).is_ok());
1204
1205 let data = json!(11);
1206 let _ = validator.validate(&data);
1209 }
1210
1211 #[test]
1212 fn test_validate_array_constraints_min_items() {
1213 let schema = json!({
1214 "type": "array",
1215 "minItems": 2
1216 });
1217
1218 let validator = Validator::from_json_schema(&schema).unwrap();
1219
1220 let data = json!([1, 2]);
1221 assert!(validator.validate(&data).is_ok());
1222
1223 let data = json!([1]);
1224 assert!(validator.validate(&data).is_err());
1225 }
1226
1227 #[test]
1228 fn test_validate_array_constraints_max_items() {
1229 let schema = json!({
1230 "type": "array",
1231 "maxItems": 2
1232 });
1233
1234 let validator = Validator::from_json_schema(&schema).unwrap();
1235
1236 let data = json!([1]);
1237 assert!(validator.validate(&data).is_ok());
1238
1239 let data = json!([1, 2, 3]);
1240 assert!(validator.validate(&data).is_err());
1241 }
1242
1243 #[test]
1244 fn test_validate_array_unique_items() {
1245 let schema = json!({
1246 "type": "array",
1247 "uniqueItems": true
1248 });
1249
1250 let validator = Validator::from_json_schema(&schema).unwrap();
1251
1252 let data = json!([1, 2, 3]);
1253 assert!(validator.validate(&data).is_ok());
1254
1255 let data = json!([1, 2, 2]);
1256 assert!(validator.validate(&data).is_err());
1257 }
1258
1259 #[test]
1260 fn test_validate_object_required_properties() {
1261 let schema = json!({
1262 "type": "object",
1263 "required": ["name", "age"]
1264 });
1265
1266 let validator = Validator::from_json_schema(&schema).unwrap();
1267
1268 let data = json!({"name": "test", "age": 25});
1269 assert!(validator.validate(&data).is_ok());
1270
1271 let data = json!({"name": "test"});
1272 assert!(validator.validate(&data).is_err());
1273 }
1274
1275 #[test]
1276 fn test_validate_content_encoding_base64() {
1277 let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1278
1279 let result = validator.validate_content_encoding(Some("SGVsbG8="), "base64", "test");
1281 assert!(result.is_ok());
1282
1283 let result = validator.validate_content_encoding(Some("not-base64!@#"), "base64", "test");
1285 assert!(result.is_err());
1286 }
1287
1288 #[test]
1289 fn test_validate_content_encoding_hex() {
1290 let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1291
1292 let result = validator.validate_content_encoding(Some("48656c6c6f"), "hex", "test");
1294 assert!(result.is_ok());
1295
1296 let result = validator.validate_content_encoding(Some("xyz"), "hex", "test");
1298 assert!(result.is_err());
1299 }
1300
1301 #[test]
1302 fn test_has_unique_items() {
1303 let validator = Validator::from_json_schema(&json!({})).unwrap();
1304
1305 let arr = vec![json!(1), json!(2), json!(3)];
1306 assert!(validator.has_unique_items(&arr));
1307
1308 let arr = vec![json!(1), json!(2), json!(1)];
1309 assert!(!validator.has_unique_items(&arr));
1310 }
1311
1312 #[test]
1313 fn test_validate_protobuf() {
1314 let result = validate_protobuf(&[], &[]);
1315 assert!(!result.valid);
1316 assert!(!result.errors.is_empty());
1317 }
1318
1319 #[test]
1320 fn test_validate_protobuf_with_type() {
1321 let result = validate_protobuf_with_type(&[], &[], "TestMessage");
1322 assert!(!result.valid);
1323 assert!(!result.errors.is_empty());
1324 }
1325
1326 #[test]
1327 fn test_is_implemented() {
1328 let json_validator = Validator::from_json_schema(&json!({"type": "object"})).unwrap();
1329 assert!(json_validator.is_implemented());
1330
1331 let openapi_validator = Validator::from_openapi(&json!({
1332 "openapi": "3.0.0",
1333 "info": {"title": "Test", "version": "1.0.0"},
1334 "paths": {}
1335 }))
1336 .unwrap();
1337 assert!(openapi_validator.is_implemented());
1338 }
1339
1340 #[test]
1345 fn test_sanitize_html() {
1346 assert_eq!(
1348 sanitize_html("<script>alert('xss')</script>"),
1349 "<script>alert('xss')</script>"
1350 );
1351
1352 assert_eq!(
1354 sanitize_html("<img src=x onerror=\"alert(1)\">"),
1355 "<img src=x onerror="alert(1)">"
1356 );
1357
1358 assert_eq!(
1360 sanitize_html("<a href=\"javascript:void(0)\">"),
1361 "<a href="javascript:void(0)">"
1362 );
1363
1364 assert_eq!(sanitize_html("&<>"), "&<>");
1366
1367 assert_eq!(
1369 sanitize_html("Hello <b>World</b> & 'Friends'"),
1370 "Hello <b>World</b> & 'Friends'"
1371 );
1372 }
1373
1374 #[test]
1375 fn test_validate_safe_path() {
1376 assert!(validate_safe_path("data/file.txt").is_ok());
1378 assert!(validate_safe_path("subdir/file.json").is_ok());
1379 assert!(validate_safe_path("file.txt").is_ok());
1380
1381 assert!(validate_safe_path("../etc/passwd").is_err());
1383 assert!(validate_safe_path("dir/../../../etc/passwd").is_err());
1384 assert!(validate_safe_path("./../../secret").is_err());
1385
1386 assert!(validate_safe_path("~/secret").is_err());
1388 assert!(validate_safe_path("dir/~/file").is_err());
1389
1390 assert!(validate_safe_path("/etc/passwd").is_err());
1392 assert!(validate_safe_path("/var/log/app.log").is_err());
1393
1394 assert!(validate_safe_path("C:\\Windows\\System32").is_err());
1396 assert!(validate_safe_path("D:\\data\\file.txt").is_err());
1397
1398 assert!(validate_safe_path("\\\\server\\share").is_err());
1400 assert!(validate_safe_path("//server/share").is_err());
1401
1402 assert!(validate_safe_path("file\0.txt").is_err());
1404
1405 assert!(validate_safe_path("dir//file.txt").is_err());
1407
1408 let result = validate_safe_path("dir\\subdir\\file.txt").unwrap();
1410 assert_eq!(result, "dir/subdir/file.txt");
1411 }
1412
1413 #[test]
1414 fn test_sanitize_sql() {
1415 assert_eq!(sanitize_sql("admin' OR '1'='1"), "admin'' OR ''1''=''1");
1417
1418 assert_eq!(sanitize_sql("'; DROP TABLE users; --"), "''; DROP TABLE users; --");
1420
1421 assert_eq!(sanitize_sql("admin"), "admin");
1423
1424 assert_eq!(sanitize_sql("O'Brien"), "O''Brien");
1426 }
1427
1428 #[test]
1429 fn test_validate_command_arg() {
1430 assert!(validate_command_arg("safe_filename.txt").is_ok());
1432 assert!(validate_command_arg("file-123.log").is_ok());
1433 assert!(validate_command_arg("data.json").is_ok());
1434
1435 assert!(validate_command_arg("file | cat /etc/passwd").is_err());
1437 assert!(validate_command_arg("file || echo pwned").is_err());
1438
1439 assert!(validate_command_arg("file; rm -rf /").is_err());
1441 assert!(validate_command_arg("file & background").is_err());
1442 assert!(validate_command_arg("file && next").is_err());
1443
1444 assert!(validate_command_arg("file > /dev/null").is_err());
1446 assert!(validate_command_arg("file < input.txt").is_err());
1447 assert!(validate_command_arg("file >> log.txt").is_err());
1448
1449 assert!(validate_command_arg("file `whoami`").is_err());
1451 assert!(validate_command_arg("file $(whoami)").is_err());
1452
1453 assert!(validate_command_arg("file*.txt").is_err());
1455 assert!(validate_command_arg("file?.log").is_err());
1456
1457 assert!(validate_command_arg("file[0-9]").is_err());
1459 assert!(validate_command_arg("file{1,2}").is_err());
1460
1461 assert!(validate_command_arg("file\0.txt").is_err());
1463
1464 assert!(validate_command_arg("file\nrm -rf /").is_err());
1466 assert!(validate_command_arg("file\rcommand").is_err());
1467
1468 assert!(validate_command_arg("file~").is_err());
1470 assert!(validate_command_arg("file!").is_err());
1471 }
1472
1473 #[test]
1474 fn test_sanitize_json_string() {
1475 assert_eq!(sanitize_json_string(r#"value","admin":true,"#), r#"value\",\"admin\":true,"#);
1477
1478 assert_eq!(sanitize_json_string(r#"C:\Windows\System32"#), r#"C:\\Windows\\System32"#);
1480
1481 assert_eq!(sanitize_json_string("line1\nline2"), r#"line1\nline2"#);
1483 assert_eq!(sanitize_json_string("tab\there"), r#"tab\there"#);
1484 assert_eq!(sanitize_json_string("carriage\rreturn"), r#"carriage\rreturn"#);
1485
1486 assert_eq!(
1488 sanitize_json_string("Test\"value\"\nNext\\line"),
1489 r#"Test\"value\"\nNext\\line"#
1490 );
1491 }
1492
1493 #[test]
1494 fn test_validate_url_safe() {
1495 assert!(validate_url_safe("https://example.com").is_ok());
1497 assert!(validate_url_safe("http://api.example.com/data").is_ok());
1498 assert!(validate_url_safe("https://subdomain.example.org:8080/path").is_ok());
1499
1500 assert!(validate_url_safe("http://localhost:8080").is_err());
1502 assert!(validate_url_safe("http://127.0.0.1").is_err());
1503 assert!(validate_url_safe("http://[::1]:8080").is_err());
1504 assert!(validate_url_safe("http://0.0.0.0").is_err());
1505
1506 assert!(validate_url_safe("http://10.0.0.1").is_err());
1508 assert!(validate_url_safe("http://192.168.1.1").is_err());
1509 assert!(validate_url_safe("http://172.16.0.1").is_err());
1510 assert!(validate_url_safe("http://172.31.255.255").is_err());
1511
1512 assert!(validate_url_safe("http://169.254.169.254/latest/meta-data").is_err());
1514
1515 assert!(validate_url_safe("http://metadata.google.internal").is_err());
1517 assert!(validate_url_safe("http://169.254.169.254").is_err());
1518
1519 assert!(validate_url_safe("HTTP://LOCALHOST:8080").is_err());
1521 assert!(validate_url_safe("http://LocalHost").is_err());
1522 }
1523
1524 #[test]
1525 fn test_sanitize_header_value() {
1526 let malicious = "value\r\nX-Evil-Header: injected";
1528 let safe = sanitize_header_value(malicious);
1529 assert!(!safe.contains('\r'));
1530 assert!(!safe.contains('\n'));
1531 assert_eq!(safe, "valueX-Evil-Header: injected");
1532
1533 let malicious = "session123\r\nSet-Cookie: admin=true";
1535 let safe = sanitize_header_value(malicious);
1536 assert_eq!(safe, "session123Set-Cookie: admin=true");
1537
1538 assert_eq!(sanitize_header_value(" value "), "value");
1540
1541 let malicious = "val\nue\r\nhe\na\rder";
1543 let safe = sanitize_header_value(malicious);
1544 assert_eq!(safe, "valueheader");
1545
1546 assert_eq!(sanitize_header_value("clean-value-123"), "clean-value-123");
1548 }
1549
1550 #[test]
1551 fn test_sanitize_html_empty_and_whitespace() {
1552 assert_eq!(sanitize_html(""), "");
1553 assert_eq!(sanitize_html(" "), " ");
1554 }
1555
1556 #[test]
1557 fn test_validate_safe_path_edge_cases() {
1558 assert!(validate_safe_path(".").is_ok());
1560
1561 assert!(validate_safe_path("README.md").is_ok());
1563
1564 assert!(validate_safe_path("a/b/c/d/e/f/file.txt").is_ok());
1566
1567 assert!(validate_safe_path("file.test.txt").is_ok());
1569
1570 assert!(validate_safe_path("..").is_err());
1572 assert!(validate_safe_path("dir/..").is_err());
1573 }
1574
1575 #[test]
1576 fn test_sanitize_sql_edge_cases() {
1577 assert_eq!(sanitize_sql(""), "");
1579
1580 assert_eq!(sanitize_sql("''"), "''''");
1582
1583 assert_eq!(sanitize_sql("'''"), "''''''");
1585 }
1586
1587 #[test]
1588 fn test_validate_command_arg_edge_cases() {
1589 assert!(validate_command_arg("").is_ok());
1591
1592 assert!(validate_command_arg("file_name-123").is_ok());
1594
1595 assert!(validate_command_arg("12345").is_ok());
1597 }
1598}