diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8ea09b7..d898667 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -318,23 +318,6 @@ pub struct LambdaExpr { pub body: LambdaBody, } -/// Lambda expression body -#[derive(Debug, Clone)] -pub enum LambdaBody { - /// Expression body: `|x| -> x + 1` - Expr(Box>), - /// Block body: `|x| { give back x + 1; }` - Block(Vec), -} - -/// Lambda/closure expression: `|x, y| -> expr` or `|x, y| { ... }` -#[derive(Debug, Clone)] -pub struct LambdaExpr { - pub params: Vec, - pub return_type: Option, - pub body: LambdaBody, -} - /// Emote tag: `@name(params)` #[derive(Debug, Clone)] pub struct EmoteTag { diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index ce4f807..ba115f2 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -32,8 +32,17 @@ pub enum RuntimeError { #[error("Index out of bounds: {0}")] IndexOutOfBounds(usize), + #[error("Negative index not allowed: {0}")] + NegativeIndex(i64), + #[error("Arity mismatch: expected {expected}, got {got}")] ArityMismatch { expected: usize, got: usize }, + + #[error("Maximum recursion depth exceeded")] + RecursionLimitExceeded, + + #[error("I/O error: {0}")] + IoError(String), } type Result = std::result::Result; @@ -92,6 +101,9 @@ impl Environment { } /// The WokeLang interpreter +/// Maximum recursion depth to prevent stack overflow +const MAX_RECURSION_DEPTH: usize = 1000; + pub struct Interpreter { env: Environment, functions: HashMap, @@ -100,6 +112,7 @@ pub struct Interpreter { consent_cache: HashMap, verbose: bool, care_mode: bool, + recursion_depth: usize, } impl Interpreter { @@ -112,6 +125,7 @@ impl Interpreter { consent_cache: HashMap::new(), verbose: false, care_mode: true, + recursion_depth: 0, } } @@ -322,10 +336,14 @@ impl Interpreter { } else { // Ask user for consent print!("Permission requested: '{}'. Allow? [y/N]: ", permission); - io::stdout().flush().unwrap(); + io::stdout() + .flush() + .map_err(|e| RuntimeError::IoError(format!("Failed to flush stdout: {}", e)))?; let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); + io::stdin() + .read_line(&mut input) + .map_err(|e| RuntimeError::IoError(format!("Failed to read input: {}", e)))?; let granted = input.trim().eq_ignore_ascii_case("y"); self.consent_cache.insert(permission.clone(), granted); @@ -566,7 +584,7 @@ impl Interpreter { let idx = match index { Value::Int(n) => { if n < 0 { - return Err(RuntimeError::IndexOutOfBounds(n as usize)); + return Err(RuntimeError::NegativeIndex(n)); } n as usize } @@ -578,11 +596,13 @@ impl Interpreter { .get(idx) .cloned() .ok_or(RuntimeError::IndexOutOfBounds(idx)), - Value::String(s) => s - .chars() - .nth(idx) - .map(|c| Value::String(c.to_string())) - .ok_or(RuntimeError::IndexOutOfBounds(idx)), + Value::String(s) => { + // Use chars().nth() for proper UTF-8 character indexing + s.chars() + .nth(idx) + .map(|c| Value::String(c.to_string())) + .ok_or(RuntimeError::IndexOutOfBounds(idx)) + } _ => Err(RuntimeError::TypeError( "Cannot index this type".into(), )), @@ -609,7 +629,8 @@ impl Interpreter { }); } match &args[0] { - Value::String(s) => Ok(Some(Value::Int(s.len() as i64))), + // Use chars().count() for proper UTF-8 character count + Value::String(s) => Ok(Some(Value::Int(s.chars().count() as i64))), Value::Array(a) => Ok(Some(Value::Int(a.len() as i64))), _ => Err(RuntimeError::TypeError("len() requires string or array".into())), } @@ -690,10 +711,18 @@ impl Interpreter { } fn call_function(&mut self, name: &str, args: Vec) -> Result { + // Check recursion depth limit + if self.recursion_depth >= MAX_RECURSION_DEPTH { + return Err(RuntimeError::RecursionLimitExceeded); + } + self.recursion_depth += 1; + // First, check if name refers to a variable holding a closure if let Some(value) = self.env.get(name).cloned() { if let Value::Function(closure) = value { - return self.call_closure(&closure, args); + let result = self.call_closure(&closure, args); + self.recursion_depth -= 1; + return result; } } @@ -702,9 +731,13 @@ impl Interpreter { .functions .get(name) .cloned() - .ok_or_else(|| RuntimeError::UndefinedFunction(name.to_string()))?; + .ok_or_else(|| { + self.recursion_depth -= 1; + RuntimeError::UndefinedFunction(name.to_string()) + })?; if func.params.len() != args.len() { + self.recursion_depth -= 1; return Err(RuntimeError::ArityMismatch { expected: func.params.len(), got: args.len(), @@ -737,6 +770,7 @@ impl Interpreter { } self.env.pop_scope(); + self.recursion_depth -= 1; // Print goodbye message if let Some(goodbye) = &func.goodbye { diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs index 67e47ef..c09f723 100644 --- a/src/interpreter/value.rs +++ b/src/interpreter/value.rs @@ -51,6 +51,8 @@ pub enum Value { String(String), Bool(bool), Array(Vec), + /// Record/object/map with string keys + Record(HashMap), Unit, /// Result success: `Okay(value)` Okay(Box), @@ -69,6 +71,7 @@ impl Value { Value::Float(f) => *f != 0.0, Value::String(s) => !s.is_empty(), Value::Array(a) => !a.is_empty(), + Value::Record(m) => !m.is_empty(), Value::Unit => false, Value::Okay(_) => true, Value::Oops(_) => false, @@ -113,6 +116,16 @@ impl fmt::Display for Value { } write!(f, "]") } + Value::Record(fields) => { + write!(f, "{{")?; + for (i, (key, val)) in fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", key, val)?; + } + write!(f, "}}") + } Value::Unit => write!(f, "()"), Value::Okay(v) => write!(f, "Okay({})", v), Value::Oops(e) => write!(f, "Oops(\"{}\")", e), diff --git a/src/lib.rs b/src/lib.rs index d64dc48..37da17a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ pub mod interpreter; pub mod lexer; pub mod parser; pub mod repl; +pub mod security; +pub mod stdlib; pub mod typechecker; pub use ast::Program; @@ -10,4 +12,6 @@ pub use interpreter::Interpreter; pub use lexer::Lexer; pub use parser::Parser; pub use repl::Repl; +pub use security::CapabilityRegistry; +pub use stdlib::StdlibRegistry; pub use typechecker::TypeChecker; diff --git a/src/security/mod.rs b/src/security/mod.rs index 47594fa..e850e9a 100644 --- a/src/security/mod.rs +++ b/src/security/mod.rs @@ -177,7 +177,8 @@ pub enum AuditAction { pub struct CapabilityRegistry { /// Granted capabilities capabilities: HashMap>, - /// Pending consent requests + /// Pending consent requests (reserved for async consent flows) + #[allow(dead_code)] pending_requests: HashSet, /// Audit log audit_log: Vec, @@ -345,7 +346,7 @@ impl CapabilityRegistry { /// Clear expired capabilities pub fn cleanup_expired(&mut self) { - for (scope, caps) in self.capabilities.iter_mut() { + for (_scope, caps) in self.capabilities.iter_mut() { caps.retain(|cap| { if !cap.is_valid() { // Log expiration if it was due to time diff --git a/src/stdlib/io.rs b/src/stdlib/io.rs index 860eb9a..cdd21e1 100644 --- a/src/stdlib/io.rs +++ b/src/stdlib/io.rs @@ -7,7 +7,52 @@ use crate::security::{Capability, CapabilityRegistry}; use super::{check_arity, check_arity_range, expect_string, StdlibError}; use std::fs; use std::io::{self, BufRead, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +/// Maximum file size for read operations (10 MB) +const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; + +/// Validate a path to prevent path traversal attacks +/// Rejects paths containing `..` components +fn validate_path(path: &str) -> Result { + let path_buf = PathBuf::from(path); + + // Check for path traversal attempts + for component in path_buf.components() { + if let std::path::Component::ParentDir = component { + return Err(StdlibError::PermissionDenied( + "Path traversal not allowed: '..' in path".to_string(), + )); + } + } + + // Reject null bytes which could be used for injection + if path.contains('\0') { + return Err(StdlibError::PermissionDenied( + "Invalid path: null byte in path".to_string(), + )); + } + + Ok(path_buf) +} + +/// Check file size before reading +fn check_file_size(path: &Path) -> Result<(), StdlibError> { + match fs::metadata(path) { + Ok(meta) => { + if meta.len() > MAX_FILE_SIZE { + Err(StdlibError::IoError(format!( + "File too large: {} bytes (max {} bytes)", + meta.len(), + MAX_FILE_SIZE + ))) + } else { + Ok(()) + } + } + Err(_) => Ok(()), // Let the actual read operation handle missing files + } +} /// Helper to require file read capability fn require_read(path: &str, caps: &mut CapabilityRegistry) -> Result<(), StdlibError> { @@ -40,9 +85,15 @@ pub fn read_file(args: &[Value], caps: &mut CapabilityRegistry) -> Result Ok(Value::String(contents)), Err(e) => Err(StdlibError::IoError(e.to_string())), } @@ -54,9 +105,12 @@ pub fn write_file(args: &[Value], caps: &mut CapabilityRegistry) -> Result Ok(Value::Bool(true)), Err(e) => Err(StdlibError::IoError(e.to_string())), } @@ -68,10 +122,13 @@ pub fn append_file(args: &[Value], caps: &mut CapabilityRegistry) -> Result match file.write_all(contents.as_bytes()) { Ok(()) => Ok(Value::Bool(true)), Err(e) => Err(StdlibError::IoError(e.to_string())), @@ -85,10 +142,13 @@ pub fn exists(args: &[Value], caps: &mut CapabilityRegistry) -> Result Result Ok(Value::Bool(true)), Err(e) => Err(StdlibError::IoError(e.to_string())), } @@ -109,9 +172,12 @@ pub fn list_dir(args: &[Value], caps: &mut CapabilityRegistry) -> Result { let files: Vec = entries .filter_map(|e| e.ok()) @@ -128,16 +194,19 @@ pub fn create_dir(args: &[Value], caps: &mut CapabilityRegistry) -> Result Ok(Value::Bool(true)), Err(e) => Err(StdlibError::IoError(e.to_string())), } } /// Read a line from stdin (interactive) -pub fn read_line(args: &[Value], caps: &mut CapabilityRegistry) -> Result { +pub fn read_line(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { check_arity_range(args, 0, 1)?; // Print optional prompt @@ -284,4 +353,36 @@ mod tests { // Cleanup let _ = fs::remove_dir_all(&dir_path); } + + #[test] + fn test_path_traversal_prevention() { + let mut caps = test_caps(); + + // Should reject paths with .. + let result = read_file(&[Value::String("../etc/passwd".to_string())], &mut caps); + assert!(result.is_err()); + + let result = read_file(&[Value::String("/tmp/../etc/passwd".to_string())], &mut caps); + assert!(result.is_err()); + + // Should reject paths with null bytes + let result = read_file(&[Value::String("test\0.txt".to_string())], &mut caps); + assert!(result.is_err()); + } + + #[test] + fn test_validate_path() { + // Valid paths should pass + assert!(validate_path("test.txt").is_ok()); + assert!(validate_path("/tmp/test.txt").is_ok()); + assert!(validate_path("./subdir/file.txt").is_ok()); + + // Path traversal should fail + assert!(validate_path("../etc/passwd").is_err()); + assert!(validate_path("/tmp/../etc/passwd").is_err()); + assert!(validate_path("a/b/../../../etc/passwd").is_err()); + + // Null bytes should fail + assert!(validate_path("test\0.txt").is_err()); + } } diff --git a/src/stdlib/json.rs b/src/stdlib/json.rs index fe2516e..fbff8c5 100644 --- a/src/stdlib/json.rs +++ b/src/stdlib/json.rs @@ -7,6 +7,12 @@ use crate::security::CapabilityRegistry; use super::{check_arity, expect_string, StdlibError}; use std::collections::HashMap; +/// Maximum JSON input size (1 MB) +const MAX_JSON_SIZE: usize = 1024 * 1024; + +/// Maximum nesting depth for JSON parsing +const MAX_NESTING_DEPTH: usize = 100; + /// Simple JSON tokenizer #[derive(Debug, Clone, PartialEq)] enum JsonToken { @@ -139,15 +145,22 @@ fn tokenize(input: &str) -> Result, StdlibError> { Ok(tokens) } -/// Parse JSON tokens into Value -fn parse_value(tokens: &[JsonToken], pos: &mut usize) -> Result { +/// Parse JSON tokens into Value with depth tracking +fn parse_value(tokens: &[JsonToken], pos: &mut usize, depth: usize) -> Result { + if depth > MAX_NESTING_DEPTH { + return Err(StdlibError::ParseError(format!( + "JSON nesting too deep (max {} levels)", + MAX_NESTING_DEPTH + ))); + } + if *pos >= tokens.len() { return Err(StdlibError::ParseError("Unexpected end of input".to_string())); } match &tokens[*pos] { - JsonToken::LBrace => parse_object(tokens, pos), - JsonToken::LBracket => parse_array(tokens, pos), + JsonToken::LBrace => parse_object(tokens, pos, depth + 1), + JsonToken::LBracket => parse_array(tokens, pos, depth + 1), JsonToken::String(s) => { *pos += 1; Ok(Value::String(s.clone())) @@ -180,7 +193,7 @@ fn parse_value(tokens: &[JsonToken], pos: &mut usize) -> Result Result { +fn parse_object(tokens: &[JsonToken], pos: &mut usize, depth: usize) -> Result { *pos += 1; // consume '{' let mut map = HashMap::new(); @@ -207,7 +220,7 @@ fn parse_object(tokens: &[JsonToken], pos: &mut usize) -> Result Result Result { +fn parse_array(tokens: &[JsonToken], pos: &mut usize, depth: usize) -> Result { *pos += 1; // consume '[' let mut items = Vec::new(); @@ -242,7 +255,7 @@ fn parse_array(tokens: &[JsonToken], pos: &mut usize) -> Result= tokens.len() { @@ -299,6 +312,7 @@ fn stringify_value(value: &Value) -> String { } Value::Okay(inner) => stringify_value(inner), Value::Oops(msg) => format!("{{\"error\":\"{}\"}}", msg), + Value::Function(_) => "null".to_string(), // Functions cannot be serialized to JSON } } @@ -307,13 +321,22 @@ pub fn parse(args: &[Value], _caps: &mut CapabilityRegistry) -> Result MAX_JSON_SIZE { + return Err(StdlibError::ParseError(format!( + "JSON input too large: {} bytes (max {} bytes)", + json_str.len(), + MAX_JSON_SIZE + ))); + } + let tokens = tokenize(&json_str)?; if tokens.is_empty() { return Err(StdlibError::ParseError("Empty JSON".to_string())); } let mut pos = 0; - let value = parse_value(&tokens, &mut pos)?; + let value = parse_value(&tokens, &mut pos, 0)?; if pos < tokens.len() { return Err(StdlibError::ParseError("Trailing content after JSON".to_string())); @@ -506,4 +529,31 @@ mod tests { _ => panic!("Expected record"), } } + + #[test] + fn test_nesting_depth_limit() { + let mut caps = test_caps(); + + // Create deeply nested JSON (150 levels, should fail at 100) + let deep_json = format!("{}1{}", "[".repeat(150), "]".repeat(150)); + + let result = parse(&[Value::String(deep_json)], &mut caps); + assert!(result.is_err()); + + // Verify error message mentions nesting + if let Err(StdlibError::ParseError(msg)) = result { + assert!(msg.contains("nesting")); + } + } + + #[test] + fn test_reasonable_nesting_ok() { + let mut caps = test_caps(); + + // Create moderately nested JSON (50 levels, should succeed) + let nested_json = format!("{}1{}", "[".repeat(50), "]".repeat(50)); + + let result = parse(&[Value::String(nested_json)], &mut caps); + assert!(result.is_ok()); + } } diff --git a/src/stdlib/net.rs b/src/stdlib/net.rs index 8fcdba2..fbb2378 100644 --- a/src/stdlib/net.rs +++ b/src/stdlib/net.rs @@ -6,9 +6,97 @@ use crate::interpreter::Value; use crate::security::{Capability, CapabilityRegistry}; use super::{check_arity, check_arity_range, expect_string, StdlibError}; use std::io::{BufRead, BufReader, Read, Write}; -use std::net::TcpStream; +use std::net::{IpAddr, TcpStream, ToSocketAddrs}; use std::time::Duration; +/// Maximum response size (10 MB) - reserved for future streaming implementation +#[allow(dead_code)] +const MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024; + +/// Validate a hostname to prevent SSRF attacks +/// Blocks requests to private/internal IP ranges and localhost +fn validate_hostname(host: &str) -> Result<(), StdlibError> { + // Reject localhost variants + let lower = host.to_lowercase(); + if lower == "localhost" || lower == "127.0.0.1" || lower == "::1" || lower == "[::1]" { + return Err(StdlibError::NetworkError( + "Access to localhost is not allowed".to_string(), + )); + } + + // Reject metadata endpoints (cloud SSRF) + if lower == "169.254.169.254" || lower.ends_with(".internal") { + return Err(StdlibError::NetworkError( + "Access to metadata endpoints is not allowed".to_string(), + )); + } + + // Try to resolve the hostname and check if it's a private IP + if let Ok(addrs) = (host, 80).to_socket_addrs() { + for addr in addrs { + if is_private_ip(&addr.ip()) { + return Err(StdlibError::NetworkError(format!( + "Access to private IP address {} is not allowed", + addr.ip() + ))); + } + } + } + + Ok(()) +} + +/// Check if an IP address is in a private/internal range +fn is_private_ip(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(ipv4) => { + // Loopback: 127.0.0.0/8 + if ipv4.is_loopback() { + return true; + } + // Private ranges + let octets = ipv4.octets(); + // 10.0.0.0/8 + if octets[0] == 10 { + return true; + } + // 172.16.0.0/12 + if octets[0] == 172 && (16..=31).contains(&octets[1]) { + return true; + } + // 192.168.0.0/16 + if octets[0] == 192 && octets[1] == 168 { + return true; + } + // Link-local: 169.254.0.0/16 + if octets[0] == 169 && octets[1] == 254 { + return true; + } + // Broadcast + if ipv4.is_broadcast() { + return true; + } + false + } + IpAddr::V6(ipv6) => { + // Loopback ::1 + if ipv6.is_loopback() { + return true; + } + // Link-local fe80::/10 + let segments = ipv6.segments(); + if (segments[0] & 0xffc0) == 0xfe80 { + return true; + } + // Unique local fc00::/7 + if (segments[0] & 0xfe00) == 0xfc00 { + return true; + } + false + } + } +} + /// Helper to require network capability fn require_network(host: &str, caps: &mut CapabilityRegistry) -> Result<(), StdlibError> { let cap = Capability::Network(Some(host.to_string())); @@ -70,6 +158,9 @@ pub fn http_get(args: &[Value], caps: &mut CapabilityRegistry) -> Result Result Result