use super::ReverseGenError;
use crate::OpenAPISpec;
use serde_json::Value;
const MALICIOUS_EXTENSION_PREFIXES: &[&str] = &[
"x-exec",
"x-eval",
"x-run",
"x-shell",
"x-script",
"x-command",
"x-code",
"x-inject",
];
const SPEC_SIGNATURE_FIELD: &str = "x-sz-orm-signature";
pub struct OpenApiInjectionGuard {
pub trust_unsigned: bool,
}
impl OpenApiInjectionGuard {
pub fn new() -> Self {
Self {
trust_unsigned: false,
}
}
pub fn with_trust_unsigned() -> Self {
Self {
trust_unsigned: true,
}
}
pub fn check(&self, spec: &OpenAPISpec) -> Result<(), ReverseGenError> {
self.check_injection(spec)?;
self.check_signature(spec)?;
Ok(())
}
fn check_injection(&self, spec: &OpenAPISpec) -> Result<(), ReverseGenError> {
let spec_json =
serde_json::to_value(spec).map_err(|e| ReverseGenError::SpecParseFailed {
path: "root".to_string(),
reason: e.to_string(),
})?;
Self::check_value_for_injection(&spec_json)
}
fn check_value_for_injection(value: &Value) -> Result<(), ReverseGenError> {
match value {
Value::Object(map) => {
for (key, val) in map {
for prefix in MALICIOUS_EXTENSION_PREFIXES {
if key == *prefix {
return Err(ReverseGenError::InjectionDetected);
}
}
Self::check_value_for_injection(val)?;
}
Ok(())
}
Value::Array(arr) => {
for item in arr {
Self::check_value_for_injection(item)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn check_signature(&self, spec: &OpenAPISpec) -> Result<(), ReverseGenError> {
if self.trust_unsigned {
return Ok(());
}
let spec_json =
serde_json::to_value(spec).map_err(|e| ReverseGenError::SpecParseFailed {
path: "root".to_string(),
reason: e.to_string(),
})?;
if Self::find_signature_field(&spec_json) {
return Ok(());
}
Err(ReverseGenError::UnsignedSpec)
}
fn find_signature_field(value: &Value) -> bool {
match value {
Value::Object(map) => {
if map.contains_key(SPEC_SIGNATURE_FIELD) {
return true;
}
for val in map.values() {
if Self::find_signature_field(val) {
return true;
}
}
false
}
Value::Array(arr) => arr.iter().any(Self::find_signature_field),
_ => false,
}
}
pub fn verify_parameterized_queries(code: &str) -> bool {
!code.contains("format!") || code.contains("where_eq") || code.contains("EDITABLE")
}
}
impl Default for OpenApiInjectionGuard {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Components, ObjectType, Schema};
use std::collections::HashMap;
fn make_safe_spec() -> OpenAPISpec {
let mut components = Components::default();
components
.schemas
.insert("User".to_string(), Schema::Object(ObjectType::new()));
OpenAPISpec {
openapi: "3.0.0".to_string(),
info: serde_json::json!({"title": "Test", "version": "1.0"}),
paths: HashMap::new(),
components: Some(components),
tags: vec![],
servers: vec![],
security: vec![],
}
}
fn make_spec_with_extension(ext_key: &str, ext_value: &str) -> OpenAPISpec {
let mut spec = make_safe_spec();
let mut info = serde_json::json!({"title": "Test", "version": "1.0"});
if let Value::Object(ref mut map) = info {
map.insert(ext_key.to_string(), Value::String(ext_value.to_string()));
}
spec.info = info;
spec
}
#[test]
fn test_safe_spec_with_trust_unsigned() {
let spec = make_safe_spec();
let guard = OpenApiInjectionGuard::with_trust_unsigned();
assert!(guard.check(&spec).is_ok());
}
#[test]
fn test_unsigned_spec_rejected() {
let spec = make_safe_spec();
let guard = OpenApiInjectionGuard::new();
let result = guard.check(&spec);
assert!(result.is_err());
match result.unwrap_err() {
ReverseGenError::UnsignedSpec => {}
_ => panic!("expected UnsignedSpec"),
}
}
#[test]
fn test_signed_spec_accepted() {
let mut spec = make_safe_spec();
let mut info = serde_json::json!({"title": "Test", "version": "1.0"});
if let Value::Object(ref mut map) = info {
map.insert(
SPEC_SIGNATURE_FIELD.to_string(),
Value::String("sha256:abc123".to_string()),
);
}
spec.info = info;
let guard = OpenApiInjectionGuard::new();
assert!(guard.check(&spec).is_ok());
}
#[test]
fn test_injection_detected_x_exec() {
let spec = make_spec_with_extension("x-exec", "rm -rf /");
let guard = OpenApiInjectionGuard::with_trust_unsigned();
let result = guard.check(&spec);
assert!(result.is_err());
match result.unwrap_err() {
ReverseGenError::InjectionDetected => {}
_ => panic!("expected InjectionDetected"),
}
}
#[test]
fn test_injection_detected_x_eval() {
let spec = make_spec_with_extension("x-eval", "dangerous code");
let guard = OpenApiInjectionGuard::with_trust_unsigned();
let result = guard.check(&spec);
assert!(result.is_err());
}
#[test]
fn test_injection_detected_x_shell() {
let spec = make_spec_with_extension("x-shell", "ls -la");
let guard = OpenApiInjectionGuard::with_trust_unsigned();
let result = guard.check(&spec);
assert!(result.is_err());
}
#[test]
fn test_no_injection_in_safe_spec() {
let spec = make_safe_spec();
let guard = OpenApiInjectionGuard::with_trust_unsigned();
assert!(guard.check(&spec).is_ok());
}
#[test]
fn test_verify_parameterized_queries() {
let safe_code = "let q = query.where_eq(\"id\", id);";
assert!(OpenApiInjectionGuard::verify_parameterized_queries(
safe_code
));
let editable_code = "// EDITABLE: business logic here";
assert!(OpenApiInjectionGuard::verify_parameterized_queries(
editable_code
));
let unsafe_code = "let q = format!(\"WHERE id = {}\", id);";
assert!(!OpenApiInjectionGuard::verify_parameterized_queries(
unsafe_code
));
}
#[test]
fn test_malicious_extension_in_paths() {
let mut spec = make_safe_spec();
let mut path_obj = serde_json::Map::new();
path_obj.insert("x-exec".to_string(), Value::String("rm -rf /".to_string()));
spec.paths
.insert("/users".to_string(), Value::Object(path_obj));
let guard = OpenApiInjectionGuard::with_trust_unsigned();
let result = guard.check(&spec);
assert!(result.is_err());
}
}