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 Err(Error::validation(
124 "Protobuf validation requires binary data — use validate_protobuf() functions directly".to_string()
125 ))
126 }
127 }
128 }
129
130 pub fn is_implemented(&self) -> bool {
132 match self {
133 Self::JsonSchema(_) => true,
134 Self::OpenApi31Schema(_, _) => true,
135 Self::OpenApi(_) => true, Self::Protobuf(_) => false, }
138 }
139
140 pub fn validate_openapi_ext(&self, data: &Value, openapi_schema: &Value) -> Result<()> {
142 match self {
143 Self::JsonSchema(_) => {
144 self.validate_openapi31_schema(data, openapi_schema)
146 }
147 Self::OpenApi31Schema(_, _) => {
148 self.validate_openapi31_schema(data, openapi_schema)
150 }
151 Self::OpenApi(_spec) => {
152 if data.is_object() {
154 Ok(())
155 } else {
156 Err(Error::validation("OpenAPI validation expects an object".to_string()))
157 }
158 }
159 Self::Protobuf(_) => {
160 Err(Error::validation(
161 "Protobuf validation requires binary data — use validate_protobuf() functions directly".to_string()
162 ))
163 }
164 }
165 }
166
167 fn validate_openapi31_schema(&self, data: &Value, schema: &Value) -> Result<()> {
169 self.validate_openapi31_constraints(data, schema, "")
170 }
171
172 fn validate_openapi31_constraints(
174 &self,
175 data: &Value,
176 schema: &Value,
177 path: &str,
178 ) -> Result<()> {
179 let schema_obj = schema
180 .as_object()
181 .ok_or_else(|| Error::validation(format!("{}: Schema must be an object", path)))?;
182
183 if let Some(type_str) = schema_obj.get("type").and_then(|v| v.as_str()) {
185 match type_str {
186 "number" | "integer" => self.validate_number_constraints(data, schema_obj, path)?,
187 "array" => self.validate_array_constraints(data, schema_obj, path)?,
188 "object" => self.validate_object_constraints(data, schema_obj, path)?,
189 "string" => self.validate_string_constraints(data, schema_obj, path)?,
190 _ => {} }
192 }
193
194 if let Some(all_of) = schema_obj.get("allOf").and_then(|v| v.as_array()) {
196 for subschema in all_of {
197 self.validate_openapi31_constraints(data, subschema, path)?;
198 }
199 }
200
201 if let Some(any_of) = schema_obj.get("anyOf").and_then(|v| v.as_array()) {
202 let mut errors = Vec::new();
203 for subschema in any_of {
204 if let Err(e) = self.validate_openapi31_constraints(data, subschema, path) {
205 errors.push(e.to_string());
206 } else {
207 return Ok(());
209 }
210 }
211 if !errors.is_empty() {
212 return Err(Error::validation(format!(
213 "{}: No subschema in anyOf matched: {}",
214 path,
215 errors.join(", ")
216 )));
217 }
218 }
219
220 if let Some(one_of) = schema_obj.get("oneOf").and_then(|v| v.as_array()) {
221 let mut matches = 0;
222 for subschema in one_of {
223 if self.validate_openapi31_constraints(data, subschema, path).is_ok() {
224 matches += 1;
225 }
226 }
227 if matches != 1 {
228 return Err(Error::validation(format!(
229 "{}: Expected exactly one subschema in oneOf to match, got {}",
230 path, matches
231 )));
232 }
233 }
234
235 if let Some(content_encoding) = schema_obj.get("contentEncoding").and_then(|v| v.as_str()) {
237 self.validate_content_encoding(data.as_str(), content_encoding, path)?;
238 }
239
240 Ok(())
241 }
242
243 fn validate_number_constraints(
245 &self,
246 data: &Value,
247 schema: &serde_json::Map<String, Value>,
248 path: &str,
249 ) -> Result<()> {
250 let num = data
251 .as_f64()
252 .ok_or_else(|| Error::validation(format!("{}: Expected number, got {}", path, data)))?;
253
254 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
256 if multiple_of > 0.0 && (num / multiple_of) % 1.0 != 0.0 {
257 return Err(Error::validation(format!(
258 "{}: {} is not a multiple of {}",
259 path, num, multiple_of
260 )));
261 }
262 }
263
264 if let Some(excl_min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
266 if num <= excl_min {
267 return Err(Error::validation(format!(
268 "{}: {} must be greater than {}",
269 path, num, excl_min
270 )));
271 }
272 }
273
274 if let Some(excl_max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
276 if num >= excl_max {
277 return Err(Error::validation(format!(
278 "{}: {} must be less than {}",
279 path, num, excl_max
280 )));
281 }
282 }
283
284 Ok(())
285 }
286
287 fn validate_array_constraints(
289 &self,
290 data: &Value,
291 schema: &serde_json::Map<String, Value>,
292 path: &str,
293 ) -> Result<()> {
294 let arr = data
295 .as_array()
296 .ok_or_else(|| Error::validation(format!("{}: Expected array, got {}", path, data)))?;
297
298 if let Some(min_items) = schema.get("minItems").and_then(|v| v.as_u64()).map(|v| v as usize)
300 {
301 if arr.len() < min_items {
302 return Err(Error::validation(format!(
303 "{}: Array has {} items, minimum is {}",
304 path,
305 arr.len(),
306 min_items
307 )));
308 }
309 }
310
311 if let Some(max_items) = schema.get("maxItems").and_then(|v| v.as_u64()).map(|v| v as usize)
313 {
314 if arr.len() > max_items {
315 return Err(Error::validation(format!(
316 "{}: Array has {} items, maximum is {}",
317 path,
318 arr.len(),
319 max_items
320 )));
321 }
322 }
323
324 if let Some(unique) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
326 if unique && !self.has_unique_items(arr) {
327 return Err(Error::validation(format!("{}: Array items must be unique", path)));
328 }
329 }
330
331 if let Some(items_schema) = schema.get("items") {
333 for (idx, item) in arr.iter().enumerate() {
334 let item_path = format!("{}[{}]", path, idx);
335 self.validate_openapi31_constraints(item, items_schema, &item_path)?;
336 }
337 }
338
339 Ok(())
340 }
341
342 fn validate_object_constraints(
344 &self,
345 data: &Value,
346 schema: &serde_json::Map<String, Value>,
347 path: &str,
348 ) -> Result<()> {
349 let obj = data
350 .as_object()
351 .ok_or_else(|| Error::validation(format!("{}: Expected object, got {}", path, data)))?;
352
353 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
355 for req_prop in required {
356 if let Some(prop_name) = req_prop.as_str() {
357 if !obj.contains_key(prop_name) {
358 return Err(Error::validation(format!(
359 "{}: Missing required property '{}'",
360 path, prop_name
361 )));
362 }
363 }
364 }
365 }
366
367 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
369 for (prop_name, prop_schema) in properties {
370 if let Some(prop_value) = obj.get(prop_name) {
371 let prop_path = format!("{}/{}", path, prop_name);
372 self.validate_openapi31_constraints(prop_value, prop_schema, &prop_path)?;
373 }
374 }
375 }
376
377 Ok(())
378 }
379
380 fn validate_string_constraints(
382 &self,
383 data: &Value,
384 schema: &serde_json::Map<String, Value>,
385 path: &str,
386 ) -> Result<()> {
387 let _str_val = data
388 .as_str()
389 .ok_or_else(|| Error::validation(format!("{}: Expected string, got {}", path, data)))?;
390
391 if schema.get("contentEncoding").is_some() {
394 }
396
397 Ok(())
398 }
399
400 fn validate_content_encoding(
402 &self,
403 data: Option<&str>,
404 encoding: &str,
405 path: &str,
406 ) -> Result<()> {
407 let str_data = data.ok_or_else(|| {
408 Error::validation(format!("{}: Content encoding requires string data", path))
409 })?;
410
411 match encoding {
412 "base64" => {
413 if general_purpose::STANDARD.decode(str_data).is_err() {
414 return Err(Error::validation(format!("{}: Invalid base64 encoding", path)));
415 }
416 }
417 "base64url" => {
418 use base64::engine::general_purpose::URL_SAFE;
419 use base64::Engine;
420 if URL_SAFE.decode(str_data).is_err() {
421 return Err(Error::validation(format!("{}: Invalid base64url encoding", path)));
422 }
423 }
424 "base32" => {
425 if base32::decode(Alphabet::Rfc4648 { padding: false }, str_data).is_none() {
426 return Err(Error::validation(format!("{}: Invalid base32 encoding", path)));
427 }
428 }
429 "hex" | "binary" => {
430 if hex::decode(str_data).is_err() {
431 return Err(Error::validation(format!(
432 "{}: Invalid {} encoding",
433 path, encoding
434 )));
435 }
436 }
437 _ => {
439 tracing::warn!(
441 "{}: Unknown content encoding '{}', skipping validation",
442 path,
443 encoding
444 );
445 }
446 }
447
448 Ok(())
449 }
450
451 fn has_unique_items(&self, arr: &[Value]) -> bool {
453 let mut seen = std::collections::HashSet::new();
454 for item in arr {
455 let item_str = serde_json::to_string(item).unwrap_or_default();
456 if !seen.insert(item_str) {
457 return false;
458 }
459 }
460 true
461 }
462}
463
464#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
466pub struct ValidationResult {
467 pub valid: bool,
469 pub errors: Vec<String>,
471 pub warnings: Vec<String>,
473}
474
475impl ValidationResult {
476 pub fn success() -> Self {
478 Self {
479 valid: true,
480 errors: Vec::new(),
481 warnings: Vec::new(),
482 }
483 }
484
485 pub fn failure(errors: Vec<String>) -> Self {
487 Self {
488 valid: false,
489 errors,
490 warnings: Vec::new(),
491 }
492 }
493
494 pub fn with_warning(mut self, warning: String) -> Self {
496 self.warnings.push(warning);
497 self
498 }
499}
500
501pub fn validate_json_schema(data: &Value, schema: &Value) -> ValidationResult {
503 match Validator::from_json_schema(schema) {
504 Ok(validator) => match validator.validate(data) {
505 Ok(_) => ValidationResult::success(),
506 Err(Error::Validation { message }) => ValidationResult::failure(vec![message]),
507 Err(e) => ValidationResult::failure(vec![format!("Unexpected error: {}", e)]),
508 },
509 Err(e) => ValidationResult::failure(vec![format!("Schema compilation error: {}", e)]),
510 }
511}
512
513pub fn validate_openapi(data: &Value, spec: &Value) -> ValidationResult {
515 let spec_obj = match spec.as_object() {
517 Some(obj) => obj,
518 None => {
519 return ValidationResult::failure(vec!["OpenAPI spec must be an object".to_string()])
520 }
521 };
522
523 let mut errors = Vec::new();
525
526 if !spec_obj.contains_key("openapi") {
527 errors.push("Missing required 'openapi' field".to_string());
528 } else if let Some(version) = spec_obj.get("openapi").and_then(|v| v.as_str()) {
529 if !version.starts_with("3.") {
530 errors.push(format!("Unsupported OpenAPI version: {}. Only 3.x is supported", version));
531 }
532 }
533
534 if !spec_obj.contains_key("info") {
535 errors.push("Missing required 'info' field".to_string());
536 } else if let Some(info) = spec_obj.get("info").and_then(|v| v.as_object()) {
537 if !info.contains_key("title") {
538 errors.push("Missing required 'info.title' field".to_string());
539 }
540 if !info.contains_key("version") {
541 errors.push("Missing required 'info.version' field".to_string());
542 }
543 }
544
545 if !spec_obj.contains_key("paths") {
546 errors.push("Missing required 'paths' field".to_string());
547 }
548
549 if !errors.is_empty() {
550 return ValidationResult::failure(errors);
551 }
552
553 if serde_json::from_value::<openapiv3::OpenAPI>(spec.clone()).is_ok() {
555 let _spec_wrapper = OpenApiSpec::from_json(spec.clone()).unwrap_or_else(|_| {
556 OpenApiSpec::from_json(json!({}))
558 .expect("Empty JSON object should always create valid OpenApiSpec")
559 });
560
561 if data.is_object() {
564 ValidationResult::success()
567 .with_warning("OpenAPI schema validation available - use validate_openapi_with_path for operation-specific validation".to_string())
568 } else {
569 ValidationResult::failure(vec![
570 "Request/response data must be a JSON object".to_string()
571 ])
572 }
573 } else {
574 ValidationResult::failure(vec!["Failed to parse OpenAPI specification".to_string()])
575 }
576}
577
578pub fn validate_openapi_operation(
580 _data: &Value,
581 spec: &OpenApiSpec,
582 path: &str,
583 method: &str,
584 _is_request: bool,
585) -> ValidationResult {
586 let mut errors = Vec::new();
587
588 if let Some(path_item_ref) = spec.spec.paths.paths.get(path) {
590 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
592 let operation = match method.to_uppercase().as_str() {
593 "GET" => path_item.get.as_ref(),
594 "POST" => path_item.post.as_ref(),
595 "PUT" => path_item.put.as_ref(),
596 "DELETE" => path_item.delete.as_ref(),
597 "PATCH" => path_item.patch.as_ref(),
598 "HEAD" => path_item.head.as_ref(),
599 "OPTIONS" => path_item.options.as_ref(),
600 _ => None,
601 };
602
603 if operation.is_some() {
604 } else {
607 errors.push(format!("Method {} not found for path {}", method, path));
608 }
609 } else {
610 errors
611 .push(format!("Path {} contains a reference, not supported for validation", path));
612 }
613 } else {
614 errors.push(format!("Path {} not found in OpenAPI spec", path));
615 }
616
617 if errors.is_empty() {
618 ValidationResult::success()
619 } else {
620 ValidationResult::failure(errors)
621 }
622}
623
624pub fn validate_protobuf(data: &[u8], descriptor_data: &[u8]) -> ValidationResult {
626 let mut pool = DescriptorPool::new();
627 if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
628 return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
629 }
630
631 let Some(message_descriptor) = pool.all_messages().next() else {
632 return ValidationResult::failure(vec![
633 "Protobuf descriptor set does not contain any message descriptors".to_string(),
634 ]);
635 };
636
637 match DynamicMessage::decode(message_descriptor, data) {
638 Ok(_) => ValidationResult::success(),
639 Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
640 }
641}
642
643pub fn validate_protobuf_message(
645 data: &[u8],
646 message_descriptor: &prost_reflect::MessageDescriptor,
647) -> Result<()> {
648 match DynamicMessage::decode(message_descriptor.clone(), data) {
650 Ok(_) => Ok(()),
651 Err(e) => Err(Error::validation(format!("Protobuf validation failed: {}", e))),
652 }
653}
654
655pub fn validate_protobuf_with_type(
657 data: &[u8],
658 descriptor_data: &[u8],
659 message_type_name: &str,
660) -> ValidationResult {
661 let mut pool = DescriptorPool::new();
662 if let Err(e) = pool.decode_file_descriptor_set(descriptor_data) {
663 return ValidationResult::failure(vec![format!("Invalid protobuf descriptor set: {}", e)]);
664 }
665
666 let descriptor = pool.get_message_by_name(message_type_name).or_else(|| {
667 pool.all_messages().find(|msg| {
668 msg.name() == message_type_name || msg.full_name().ends_with(message_type_name)
669 })
670 });
671
672 let Some(message_descriptor) = descriptor else {
673 return ValidationResult::failure(vec![format!(
674 "Message type '{}' not found in descriptor set",
675 message_type_name
676 )]);
677 };
678
679 match DynamicMessage::decode(message_descriptor, data) {
680 Ok(_) => ValidationResult::success(),
681 Err(e) => ValidationResult::failure(vec![format!("Protobuf validation failed: {}", e)]),
682 }
683}
684
685pub fn validate_openapi_security(
687 spec: &OpenApiSpec,
688 security_requirements: &[OpenApiSecurityRequirement],
689 auth_header: Option<&str>,
690 api_key: Option<&str>,
691) -> ValidationResult {
692 match spec.validate_security_requirements(security_requirements, auth_header, api_key) {
693 Ok(_) => ValidationResult::success(),
694 Err(e) => ValidationResult::failure(vec![format!("Security validation failed: {}", e)]),
695 }
696}
697
698pub fn validate_openapi_operation_security(
700 spec: &OpenApiSpec,
701 path: &str,
702 method: &str,
703 auth_header: Option<&str>,
704 api_key: Option<&str>,
705) -> ValidationResult {
706 let operations = spec.operations_for_path(path);
708
709 let operation = operations
711 .iter()
712 .find(|(op_method, _)| op_method.to_uppercase() == method.to_uppercase());
713
714 let operation = match operation {
715 Some((_, op)) => op,
716 None => {
717 return ValidationResult::failure(vec![format!(
718 "Operation not found: {} {}",
719 method, path
720 )])
721 }
722 };
723
724 let openapi_operation =
726 OpenApiOperation::from_operation(method, path.to_string(), operation, spec);
727
728 if let Some(ref security_reqs) = openapi_operation.security {
730 if !security_reqs.is_empty() {
731 return validate_openapi_security(spec, security_reqs, auth_header, api_key);
732 }
733 }
734
735 let global_security = spec.get_global_security_requirements();
737 if !global_security.is_empty() {
738 return validate_openapi_security(spec, &global_security, auth_header, api_key);
739 }
740
741 ValidationResult::success()
743}
744
745pub fn sanitize_html(input: &str) -> String {
763 input
764 .replace('&', "&")
765 .replace('<', "<")
766 .replace('>', ">")
767 .replace('"', """)
768 .replace('\'', "'")
769 .replace('/', "/")
770}
771
772pub fn validate_safe_path(path: &str) -> Result<String> {
792 if path.contains('\0') {
794 return Err(Error::validation("Path contains null bytes".to_string()));
795 }
796
797 if path.contains("..") {
799 return Err(Error::validation("Path traversal detected: '..' not allowed".to_string()));
800 }
801
802 if path.contains('~') {
804 return Err(Error::validation("Home directory expansion '~' not allowed".to_string()));
805 }
806
807 if path.starts_with('/') {
809 return Err(Error::validation("Absolute paths not allowed".to_string()));
810 }
811
812 if path.len() >= 2 && path.chars().nth(1) == Some(':') {
814 return Err(Error::validation("Absolute paths with drive letters not allowed".to_string()));
815 }
816
817 if path.starts_with("\\\\") || path.starts_with("//") {
819 return Err(Error::validation("UNC paths not allowed".to_string()));
820 }
821
822 let normalized = path.replace('\\', "/");
824
825 if normalized.contains("//") {
827 return Err(Error::validation("Path contains empty segments".to_string()));
828 }
829
830 Ok(normalized)
831}
832
833pub fn sanitize_sql(input: &str) -> String {
850 input.replace('\'', "''")
852}
853
854pub fn validate_command_arg(arg: &str) -> Result<String> {
877 let dangerous_chars = [
879 '|', ';', '&', '<', '>', '`', '$', '(', ')', '*', '?', '[', ']', '{', '}', '~', '!', '\n',
880 '\r', '\0',
881 ];
882
883 for ch in dangerous_chars.iter() {
884 if arg.contains(*ch) {
885 return Err(Error::validation(format!(
886 "Command argument contains dangerous character: '{}'",
887 ch
888 )));
889 }
890 }
891
892 if arg.contains("$(") {
894 return Err(Error::validation("Command substitution pattern '$(' not allowed".to_string()));
895 }
896
897 Ok(arg.to_string())
898}
899
900pub fn sanitize_json_string(input: &str) -> String {
914 input
915 .replace('\\', "\\\\") .replace('"', "\\\"")
917 .replace('\n', "\\n")
918 .replace('\r', "\\r")
919 .replace('\t', "\\t")
920}
921
922pub fn validate_url_safe(url: &str) -> Result<String> {
942 let url_lower = url.to_lowercase();
944
945 let localhost_patterns = ["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"];
947 for pattern in localhost_patterns.iter() {
948 if url_lower.contains(pattern) {
949 return Err(Error::validation(
950 "URLs pointing to localhost are not allowed".to_string(),
951 ));
952 }
953 }
954
955 let private_ranges = [
957 "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.",
958 "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.",
959 "172.31.", "192.168.",
960 ];
961 for range in private_ranges.iter() {
962 if url_lower.contains(range) {
963 return Err(Error::validation(format!(
964 "URLs pointing to private IP range '{}' are not allowed",
965 range
966 )));
967 }
968 }
969
970 if url_lower.contains("169.254.") {
972 return Err(Error::validation(
973 "URLs pointing to link-local addresses (169.254.x) are not allowed".to_string(),
974 ));
975 }
976
977 let metadata_endpoints = [
979 "metadata.google.internal",
980 "169.254.169.254", "fd00:ec2::254", ];
983 for endpoint in metadata_endpoints.iter() {
984 if url_lower.contains(endpoint) {
985 return Err(Error::validation(format!(
986 "URLs pointing to cloud metadata endpoint '{}' are not allowed",
987 endpoint
988 )));
989 }
990 }
991
992 Ok(url.to_string())
993}
994
995pub fn sanitize_header_value(input: &str) -> String {
1010 input.replace(['\r', '\n'], "").trim().to_string()
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017
1018 #[test]
1019 fn test_validation_result_success() {
1020 let result = ValidationResult::success();
1021 assert!(result.valid);
1022 assert!(result.errors.is_empty());
1023 assert!(result.warnings.is_empty());
1024 }
1025
1026 #[test]
1027 fn test_validation_result_failure() {
1028 let errors = vec!["error1".to_string(), "error2".to_string()];
1029 let result = ValidationResult::failure(errors.clone());
1030 assert!(!result.valid);
1031 assert_eq!(result.errors, errors);
1032 assert!(result.warnings.is_empty());
1033 }
1034
1035 #[test]
1036 fn test_validation_result_with_warning() {
1037 let result = ValidationResult::success()
1038 .with_warning("warning1".to_string())
1039 .with_warning("warning2".to_string());
1040 assert!(result.valid);
1041 assert_eq!(result.warnings.len(), 2);
1042 }
1043
1044 #[test]
1045 fn test_validator_from_json_schema() {
1046 let schema = json!({
1047 "type": "object",
1048 "properties": {
1049 "name": {"type": "string"}
1050 }
1051 });
1052
1053 let validator = Validator::from_json_schema(&schema);
1054 assert!(validator.is_ok());
1055 assert!(validator.unwrap().is_implemented());
1056 }
1057
1058 #[test]
1059 fn test_validator_from_json_schema_invalid() {
1060 let schema = json!({
1061 "type": "invalid_type"
1062 });
1063
1064 let validator = Validator::from_json_schema(&schema);
1066 assert!(validator.is_err());
1067 }
1068
1069 #[test]
1070 fn test_validator_validate_json_schema_success() {
1071 let schema = json!({
1072 "type": "object",
1073 "properties": {
1074 "name": {"type": "string"}
1075 }
1076 });
1077
1078 let validator = Validator::from_json_schema(&schema).unwrap();
1079 let data = json!({"name": "test"});
1080
1081 assert!(validator.validate(&data).is_ok());
1082 }
1083
1084 #[test]
1085 fn test_validator_validate_json_schema_failure() {
1086 let schema = json!({
1087 "type": "object",
1088 "properties": {
1089 "name": {"type": "string"}
1090 }
1091 });
1092
1093 let validator = Validator::from_json_schema(&schema).unwrap();
1094 let data = json!({"name": 123});
1095
1096 assert!(validator.validate(&data).is_err());
1097 }
1098
1099 #[test]
1100 fn test_validator_from_openapi() {
1101 let spec = json!({
1102 "openapi": "3.0.0",
1103 "info": {"title": "Test", "version": "1.0.0"},
1104 "paths": {}
1105 });
1106
1107 let validator = Validator::from_openapi(&spec);
1108 assert!(validator.is_ok());
1109 }
1110
1111 #[test]
1112 fn test_validator_from_openapi_unsupported_version() {
1113 let spec = json!({
1114 "openapi": "2.0.0",
1115 "info": {"title": "Test", "version": "1.0.0"},
1116 "paths": {}
1117 });
1118
1119 let validator = Validator::from_openapi(&spec);
1120 assert!(validator.is_err());
1121 }
1122
1123 #[test]
1124 fn test_validator_validate_openapi() {
1125 let spec = json!({
1126 "openapi": "3.0.0",
1127 "info": {"title": "Test", "version": "1.0.0"},
1128 "paths": {}
1129 });
1130
1131 let validator = Validator::from_openapi(&spec).unwrap();
1132 let data = json!({"key": "value"});
1133
1134 assert!(validator.validate(&data).is_ok());
1135 }
1136
1137 #[test]
1138 fn test_validator_validate_openapi_non_object() {
1139 let spec = json!({
1140 "openapi": "3.0.0",
1141 "info": {"title": "Test", "version": "1.0.0"},
1142 "paths": {}
1143 });
1144
1145 let validator = Validator::from_openapi(&spec).unwrap();
1146 let data = json!("string");
1147
1148 assert!(validator.validate(&data).is_err());
1149 }
1150
1151 #[test]
1152 fn test_validate_json_schema_function() {
1153 let schema = json!({
1154 "type": "object",
1155 "properties": {
1156 "age": {"type": "number"}
1157 }
1158 });
1159
1160 let data = json!({"age": 25});
1161 let result = validate_json_schema(&data, &schema);
1162 assert!(result.valid);
1163
1164 let data = json!({"age": "25"});
1165 let result = validate_json_schema(&data, &schema);
1166 assert!(!result.valid);
1167 }
1168
1169 #[test]
1170 fn test_validate_openapi_function() {
1171 let spec = json!({
1172 "openapi": "3.0.0",
1173 "info": {"title": "Test", "version": "1.0.0"},
1174 "paths": {}
1175 });
1176
1177 let data = json!({"test": "value"});
1178 let result = validate_openapi(&data, &spec);
1179 assert!(result.valid);
1180 }
1181
1182 #[test]
1183 fn test_validate_openapi_missing_fields() {
1184 let spec = json!({
1185 "openapi": "3.0.0"
1186 });
1187
1188 let data = json!({});
1189 let result = validate_openapi(&data, &spec);
1190 assert!(!result.valid);
1191 assert!(!result.errors.is_empty());
1192 }
1193
1194 #[test]
1195 fn test_validate_number_constraints_multiple_of() {
1196 let schema = json!({
1197 "type": "number",
1198 "multipleOf": 5.0
1199 });
1200
1201 let validator = Validator::from_json_schema(&schema).unwrap();
1202
1203 let data = json!(10);
1204 assert!(validator.validate(&data).is_ok());
1205
1206 let data = json!(11);
1207 let _ = validator.validate(&data);
1210 }
1211
1212 #[test]
1213 fn test_validate_array_constraints_min_items() {
1214 let schema = json!({
1215 "type": "array",
1216 "minItems": 2
1217 });
1218
1219 let validator = Validator::from_json_schema(&schema).unwrap();
1220
1221 let data = json!([1, 2]);
1222 assert!(validator.validate(&data).is_ok());
1223
1224 let data = json!([1]);
1225 assert!(validator.validate(&data).is_err());
1226 }
1227
1228 #[test]
1229 fn test_validate_array_constraints_max_items() {
1230 let schema = json!({
1231 "type": "array",
1232 "maxItems": 2
1233 });
1234
1235 let validator = Validator::from_json_schema(&schema).unwrap();
1236
1237 let data = json!([1]);
1238 assert!(validator.validate(&data).is_ok());
1239
1240 let data = json!([1, 2, 3]);
1241 assert!(validator.validate(&data).is_err());
1242 }
1243
1244 #[test]
1245 fn test_validate_array_unique_items() {
1246 let schema = json!({
1247 "type": "array",
1248 "uniqueItems": true
1249 });
1250
1251 let validator = Validator::from_json_schema(&schema).unwrap();
1252
1253 let data = json!([1, 2, 3]);
1254 assert!(validator.validate(&data).is_ok());
1255
1256 let data = json!([1, 2, 2]);
1257 assert!(validator.validate(&data).is_err());
1258 }
1259
1260 #[test]
1261 fn test_validate_object_required_properties() {
1262 let schema = json!({
1263 "type": "object",
1264 "required": ["name", "age"]
1265 });
1266
1267 let validator = Validator::from_json_schema(&schema).unwrap();
1268
1269 let data = json!({"name": "test", "age": 25});
1270 assert!(validator.validate(&data).is_ok());
1271
1272 let data = json!({"name": "test"});
1273 assert!(validator.validate(&data).is_err());
1274 }
1275
1276 #[test]
1277 fn test_validate_content_encoding_base64() {
1278 let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1279
1280 let result = validator.validate_content_encoding(Some("SGVsbG8="), "base64", "test");
1282 assert!(result.is_ok());
1283
1284 let result = validator.validate_content_encoding(Some("not-base64!@#"), "base64", "test");
1286 assert!(result.is_err());
1287 }
1288
1289 #[test]
1290 fn test_validate_content_encoding_hex() {
1291 let validator = Validator::from_json_schema(&json!({"type": "string"})).unwrap();
1292
1293 let result = validator.validate_content_encoding(Some("48656c6c6f"), "hex", "test");
1295 assert!(result.is_ok());
1296
1297 let result = validator.validate_content_encoding(Some("xyz"), "hex", "test");
1299 assert!(result.is_err());
1300 }
1301
1302 #[test]
1303 fn test_has_unique_items() {
1304 let validator = Validator::from_json_schema(&json!({})).unwrap();
1305
1306 let arr = vec![json!(1), json!(2), json!(3)];
1307 assert!(validator.has_unique_items(&arr));
1308
1309 let arr = vec![json!(1), json!(2), json!(1)];
1310 assert!(!validator.has_unique_items(&arr));
1311 }
1312
1313 #[test]
1314 fn test_validate_protobuf() {
1315 let result = validate_protobuf(&[], &[]);
1316 assert!(!result.valid);
1317 assert!(!result.errors.is_empty());
1318 }
1319
1320 #[test]
1321 fn test_validate_protobuf_with_type() {
1322 let result = validate_protobuf_with_type(&[], &[], "TestMessage");
1323 assert!(!result.valid);
1324 assert!(!result.errors.is_empty());
1325 }
1326
1327 #[test]
1328 fn test_is_implemented() {
1329 let json_validator = Validator::from_json_schema(&json!({"type": "object"})).unwrap();
1330 assert!(json_validator.is_implemented());
1331
1332 let openapi_validator = Validator::from_openapi(&json!({
1333 "openapi": "3.0.0",
1334 "info": {"title": "Test", "version": "1.0.0"},
1335 "paths": {}
1336 }))
1337 .unwrap();
1338 assert!(openapi_validator.is_implemented());
1339 }
1340
1341 #[test]
1346 fn test_sanitize_html() {
1347 assert_eq!(
1349 sanitize_html("<script>alert('xss')</script>"),
1350 "<script>alert('xss')</script>"
1351 );
1352
1353 assert_eq!(
1355 sanitize_html("<img src=x onerror=\"alert(1)\">"),
1356 "<img src=x onerror="alert(1)">"
1357 );
1358
1359 assert_eq!(
1361 sanitize_html("<a href=\"javascript:void(0)\">"),
1362 "<a href="javascript:void(0)">"
1363 );
1364
1365 assert_eq!(sanitize_html("&<>"), "&<>");
1367
1368 assert_eq!(
1370 sanitize_html("Hello <b>World</b> & 'Friends'"),
1371 "Hello <b>World</b> & 'Friends'"
1372 );
1373 }
1374
1375 #[test]
1376 fn test_validate_safe_path() {
1377 assert!(validate_safe_path("data/file.txt").is_ok());
1379 assert!(validate_safe_path("subdir/file.json").is_ok());
1380 assert!(validate_safe_path("file.txt").is_ok());
1381
1382 assert!(validate_safe_path("../etc/passwd").is_err());
1384 assert!(validate_safe_path("dir/../../../etc/passwd").is_err());
1385 assert!(validate_safe_path("./../../secret").is_err());
1386
1387 assert!(validate_safe_path("~/secret").is_err());
1389 assert!(validate_safe_path("dir/~/file").is_err());
1390
1391 assert!(validate_safe_path("/etc/passwd").is_err());
1393 assert!(validate_safe_path("/var/log/app.log").is_err());
1394
1395 assert!(validate_safe_path("C:\\Windows\\System32").is_err());
1397 assert!(validate_safe_path("D:\\data\\file.txt").is_err());
1398
1399 assert!(validate_safe_path("\\\\server\\share").is_err());
1401 assert!(validate_safe_path("//server/share").is_err());
1402
1403 assert!(validate_safe_path("file\0.txt").is_err());
1405
1406 assert!(validate_safe_path("dir//file.txt").is_err());
1408
1409 let result = validate_safe_path("dir\\subdir\\file.txt").unwrap();
1411 assert_eq!(result, "dir/subdir/file.txt");
1412 }
1413
1414 #[test]
1415 fn test_sanitize_sql() {
1416 assert_eq!(sanitize_sql("admin' OR '1'='1"), "admin'' OR ''1''=''1");
1418
1419 assert_eq!(sanitize_sql("'; DROP TABLE users; --"), "''; DROP TABLE users; --");
1421
1422 assert_eq!(sanitize_sql("admin"), "admin");
1424
1425 assert_eq!(sanitize_sql("O'Brien"), "O''Brien");
1427 }
1428
1429 #[test]
1430 fn test_validate_command_arg() {
1431 assert!(validate_command_arg("safe_filename.txt").is_ok());
1433 assert!(validate_command_arg("file-123.log").is_ok());
1434 assert!(validate_command_arg("data.json").is_ok());
1435
1436 assert!(validate_command_arg("file | cat /etc/passwd").is_err());
1438 assert!(validate_command_arg("file || echo pwned").is_err());
1439
1440 assert!(validate_command_arg("file; rm -rf /").is_err());
1442 assert!(validate_command_arg("file & background").is_err());
1443 assert!(validate_command_arg("file && next").is_err());
1444
1445 assert!(validate_command_arg("file > /dev/null").is_err());
1447 assert!(validate_command_arg("file < input.txt").is_err());
1448 assert!(validate_command_arg("file >> log.txt").is_err());
1449
1450 assert!(validate_command_arg("file `whoami`").is_err());
1452 assert!(validate_command_arg("file $(whoami)").is_err());
1453
1454 assert!(validate_command_arg("file*.txt").is_err());
1456 assert!(validate_command_arg("file?.log").is_err());
1457
1458 assert!(validate_command_arg("file[0-9]").is_err());
1460 assert!(validate_command_arg("file{1,2}").is_err());
1461
1462 assert!(validate_command_arg("file\0.txt").is_err());
1464
1465 assert!(validate_command_arg("file\nrm -rf /").is_err());
1467 assert!(validate_command_arg("file\rcommand").is_err());
1468
1469 assert!(validate_command_arg("file~").is_err());
1471 assert!(validate_command_arg("file!").is_err());
1472 }
1473
1474 #[test]
1475 fn test_sanitize_json_string() {
1476 assert_eq!(sanitize_json_string(r#"value","admin":true,"#), r#"value\",\"admin\":true,"#);
1478
1479 assert_eq!(sanitize_json_string(r#"C:\Windows\System32"#), r#"C:\\Windows\\System32"#);
1481
1482 assert_eq!(sanitize_json_string("line1\nline2"), r#"line1\nline2"#);
1484 assert_eq!(sanitize_json_string("tab\there"), r#"tab\there"#);
1485 assert_eq!(sanitize_json_string("carriage\rreturn"), r#"carriage\rreturn"#);
1486
1487 assert_eq!(
1489 sanitize_json_string("Test\"value\"\nNext\\line"),
1490 r#"Test\"value\"\nNext\\line"#
1491 );
1492 }
1493
1494 #[test]
1495 fn test_validate_url_safe() {
1496 assert!(validate_url_safe("https://example.com").is_ok());
1498 assert!(validate_url_safe("http://api.example.com/data").is_ok());
1499 assert!(validate_url_safe("https://subdomain.example.org:8080/path").is_ok());
1500
1501 assert!(validate_url_safe("http://localhost:8080").is_err());
1503 assert!(validate_url_safe("http://127.0.0.1").is_err());
1504 assert!(validate_url_safe("http://[::1]:8080").is_err());
1505 assert!(validate_url_safe("http://0.0.0.0").is_err());
1506
1507 assert!(validate_url_safe("http://10.0.0.1").is_err());
1509 assert!(validate_url_safe("http://192.168.1.1").is_err());
1510 assert!(validate_url_safe("http://172.16.0.1").is_err());
1511 assert!(validate_url_safe("http://172.31.255.255").is_err());
1512
1513 assert!(validate_url_safe("http://169.254.169.254/latest/meta-data").is_err());
1515
1516 assert!(validate_url_safe("http://metadata.google.internal").is_err());
1518 assert!(validate_url_safe("http://169.254.169.254").is_err());
1519
1520 assert!(validate_url_safe("HTTP://LOCALHOST:8080").is_err());
1522 assert!(validate_url_safe("http://LocalHost").is_err());
1523 }
1524
1525 #[test]
1526 fn test_sanitize_header_value() {
1527 let malicious = "value\r\nX-Evil-Header: injected";
1529 let safe = sanitize_header_value(malicious);
1530 assert!(!safe.contains('\r'));
1531 assert!(!safe.contains('\n'));
1532 assert_eq!(safe, "valueX-Evil-Header: injected");
1533
1534 let malicious = "session123\r\nSet-Cookie: admin=true";
1536 let safe = sanitize_header_value(malicious);
1537 assert_eq!(safe, "session123Set-Cookie: admin=true");
1538
1539 assert_eq!(sanitize_header_value(" value "), "value");
1541
1542 let malicious = "val\nue\r\nhe\na\rder";
1544 let safe = sanitize_header_value(malicious);
1545 assert_eq!(safe, "valueheader");
1546
1547 assert_eq!(sanitize_header_value("clean-value-123"), "clean-value-123");
1549 }
1550
1551 #[test]
1552 fn test_sanitize_html_empty_and_whitespace() {
1553 assert_eq!(sanitize_html(""), "");
1554 assert_eq!(sanitize_html(" "), " ");
1555 }
1556
1557 #[test]
1558 fn test_validate_safe_path_edge_cases() {
1559 assert!(validate_safe_path(".").is_ok());
1561
1562 assert!(validate_safe_path("README.md").is_ok());
1564
1565 assert!(validate_safe_path("a/b/c/d/e/f/file.txt").is_ok());
1567
1568 assert!(validate_safe_path("file.test.txt").is_ok());
1570
1571 assert!(validate_safe_path("..").is_err());
1573 assert!(validate_safe_path("dir/..").is_err());
1574 }
1575
1576 #[test]
1577 fn test_sanitize_sql_edge_cases() {
1578 assert_eq!(sanitize_sql(""), "");
1580
1581 assert_eq!(sanitize_sql("''"), "''''");
1583
1584 assert_eq!(sanitize_sql("'''"), "''''''");
1586 }
1587
1588 #[test]
1589 fn test_validate_command_arg_edge_cases() {
1590 assert!(validate_command_arg("").is_ok());
1592
1593 assert!(validate_command_arg("file_name-123").is_ok());
1595
1596 assert!(validate_command_arg("12345").is_ok());
1598 }
1599}