#![allow(unused_variables, unused_imports)]
use bashkit::{Bash, ExecutionLimits};
use std::sync::Arc;
use std::time::{Duration, Instant};
mod internal_variable_injection {
use super::*;
#[tokio::test]
async fn security_audit_declare_blocks_nameref_prefix() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
secret="sensitive_data"
declare _NAMEREF_alias=secret
echo "$alias"
"#,
)
.await
.unwrap();
assert_ne!(
result.stdout.trim(),
"sensitive_data",
"declare must block _NAMEREF_ prefix injection"
);
}
#[tokio::test]
async fn security_audit_readonly_blocks_nameref_prefix() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
target="important_value"
readonly _NAMEREF_sneaky=target
echo "$sneaky"
"#,
)
.await
.unwrap();
assert_ne!(
result.stdout.trim(),
"important_value",
"readonly must block _NAMEREF_ prefix injection"
);
}
#[tokio::test]
async fn security_audit_declare_blocks_upper_prefix() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
declare _UPPER_myvar=1
myvar="should be lowercase"
echo "$myvar"
"#,
)
.await
.unwrap();
assert_eq!(
result.stdout.trim(),
"should be lowercase",
"declare must block _UPPER_ prefix injection"
);
}
#[tokio::test]
async fn security_audit_declare_blocks_lower_prefix() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
declare _LOWER_myvar=1
myvar="SHOULD BE UPPERCASE"
echo "$myvar"
"#,
)
.await
.unwrap();
assert_eq!(
result.stdout.trim(),
"SHOULD BE UPPERCASE",
"declare must block _LOWER_ prefix injection"
);
}
#[tokio::test]
async fn security_audit_array_read_prefix_blocked() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
"export \"_ARRAY_READ_injected=val0\x1Fval1\x1Fval2\"\ntrue\necho \"${injected[0]} ${injected[1]} ${injected[2]}\"",
)
.await
.unwrap();
assert!(
!result.stdout.trim().contains("val0"),
"_ARRAY_READ_ prefix must be blocked. Got: '{}'",
result.stdout.trim()
);
}
#[tokio::test]
async fn security_audit_export_blocks_readonly_prefix() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
myvar="original"
export _READONLY_myvar=1
myvar="changed"
echo "$myvar"
"#,
)
.await
.unwrap();
assert_eq!(result.stdout.trim(), "changed");
let leak = bash.exec("set | grep _READONLY_myvar").await.unwrap();
assert!(
leak.stdout.trim().is_empty(),
"_READONLY_ marker must not be injectable via export"
);
}
#[tokio::test]
async fn security_audit_local_blocks_internal_prefixes() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
secret="stolen"
local _NAMEREF_sneaky=secret
echo "$sneaky"
"#,
)
.await
.unwrap();
assert_ne!(
result.stdout.trim(),
"stolen",
"local must block _NAMEREF_ prefix injection"
);
}
}
mod internal_variable_leak {
use super::*;
#[tokio::test]
async fn security_audit_set_hides_internal_markers() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
declare -n myref=target
readonly myval=123
set | grep -E "^_(NAMEREF|READONLY)_"
"#,
)
.await
.unwrap();
assert!(
result.stdout.trim().is_empty(),
"`set` must filter internal markers from output. Got:\n{}",
result.stdout.trim()
);
}
#[tokio::test]
async fn security_audit_declare_p_hides_internal_markers() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
declare -n myref=target
readonly locked=42
declare -p | grep -E "_(NAMEREF|READONLY)_"
"#,
)
.await
.unwrap();
assert!(
result.stdout.trim().is_empty(),
"`declare -p` must filter internal markers. Got:\n{}",
result.stdout.trim()
);
}
#[tokio::test]
async fn security_audit_set_hides_shopt_vars() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
set -e
set -o pipefail
set | grep -E "^SHOPT_"
"#,
)
.await
.unwrap();
assert!(
result.stdout.trim().is_empty(),
"`set` must filter SHOPT_ internal variables from output. Got:\n{}",
result.stdout.trim()
);
}
#[tokio::test]
async fn security_audit_declare_p_hides_shopt_vars() {
let mut bash = Bash::builder().build();
let result = bash
.exec(
r#"
set -e
set -o pipefail
declare -p | grep -E "SHOPT_"
"#,
)
.await
.unwrap();
assert!(
result.stdout.trim().is_empty(),
"`declare -p` must filter SHOPT_ internal variables. Got:\n{}",
result.stdout.trim()
);
}
}
mod arithmetic_overflow {
use super::*;
#[tokio::test]
async fn security_audit_compound_add_no_panic() {
let limits = ExecutionLimits::new().timeout(Duration::from_secs(5));
let mut bash = Bash::builder().limits(limits).build();
let result = bash.exec("x=9223372036854775807; ((x+=1)); echo $x").await;
assert!(
result.is_ok(),
"Compound += with i64::MAX must not panic. Got: {:?}",
result.err()
);
}
#[tokio::test]
async fn security_audit_compound_shift_clamped() {
let limits = ExecutionLimits::new().timeout(Duration::from_secs(5));
let mut bash = Bash::builder().limits(limits).build();
let result = bash.exec("x=1; let 'x<<=64'; echo $x").await;
assert!(
result.is_ok(),
"Compound <<= with shift>=64 must not panic. Got: {:?}",
result.err()
);
}
}
mod vfs_limit_bypass {
use super::*;
use bashkit::{FileSystem, FsLimits, InMemoryFs};
use std::path::Path;
#[tokio::test]
async fn security_audit_copy_enforces_limit_on_overwrite() {
let limits = FsLimits::new()
.max_total_bytes(600)
.max_file_size(600)
.max_file_count(10);
let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/target"), b"tiny_file!")
.await
.unwrap();
fs.write_file(Path::new("/source"), &vec![b'A'; 500])
.await
.unwrap();
let result = fs.copy(Path::new("/source"), Path::new("/target")).await;
assert!(
result.is_err(),
"copy() must enforce size limits on overwrite"
);
}
#[tokio::test]
async fn security_audit_rename_rejects_file_over_dir() {
let fs = InMemoryFs::new();
fs.mkdir(Path::new("/mydir"), false).await.unwrap();
fs.write_file(Path::new("/mydir/child.txt"), b"child data")
.await
.unwrap();
fs.write_file(Path::new("/myfile"), b"file data")
.await
.unwrap();
let result = fs.rename(Path::new("/myfile"), Path::new("/mydir")).await;
assert!(result.is_err(), "rename(file, dir) must fail per POSIX");
}
}
mod overlay_symlink_bypass {
use super::*;
use bashkit::{FileSystem, FsLimits, InMemoryFs, OverlayFs};
use std::path::Path;
#[tokio::test]
async fn security_audit_overlay_symlink_enforces_limit() {
let lower: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
let limits = FsLimits::new().max_file_count(11);
let overlay = OverlayFs::with_limits(lower, limits);
for i in 0..5 {
let link = format!("/link{}", i);
overlay
.symlink(Path::new("/target"), Path::new(&link))
.await
.unwrap();
}
let result = overlay
.symlink(Path::new("/target"), Path::new("/link_overflow"))
.await;
assert!(
result.is_err(),
"symlink() must reject creation beyond max_file_count"
);
}
}
mod information_disclosure {
use super::*;
#[tokio::test]
async fn security_audit_date_uses_virtual_time() {
let fixed = 1577836800i64;
let mut bash = Bash::builder()
.username("sandboxuser")
.hostname("sandbox.local")
.fixed_epoch(fixed)
.build();
let host = bash.exec("hostname").await.unwrap();
assert_eq!(host.stdout.trim(), "sandbox.local");
let result = bash.exec("date +%s").await.unwrap();
let script_epoch: i64 = result.stdout.trim().parse().unwrap_or(0);
assert_eq!(
script_epoch, fixed,
"date must use fixed epoch, not real host clock (got={})",
script_epoch
);
}
}
mod brace_expansion_dos {
use super::*;
#[tokio::test]
async fn security_audit_brace_expansion_capped() {
let limits = ExecutionLimits::new()
.max_commands(100)
.timeout(Duration::from_secs(10));
let mut bash = Bash::builder().limits(limits).build();
let result = bash.exec("echo {1..1000000}").await;
assert!(
result.is_err(),
"Brace expansion with 1M elements must be rejected"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("brace range too large"),
"Expected budget validation error, got: {err}"
);
let result = bash.exec("echo {1..3}").await.unwrap();
assert_eq!(result.stdout.trim(), "1 2 3");
}
}
mod lexer_stack_overflow {
use super::*;
#[tokio::test]
async fn security_audit_nested_subst_graceful_error() {
let limits = ExecutionLimits::new()
.max_ast_depth(10)
.timeout(Duration::from_secs(5));
let mut bash = Bash::builder().limits(limits).build();
let mut script = String::new();
let depth = 15;
for _ in 0..depth {
script.push_str("echo \"$(");
}
script.push_str("echo hi");
for _ in 0..depth {
script.push_str(")\"");
}
let result = bash.exec(&script).await;
match result {
Ok(_) => {} Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("stack overflow"),
"Must fail with depth limit, not stack overflow: {}",
msg
);
}
}
}
}
mod mountable_fs_validate_path {
use super::*;
use bashkit::{FileSystem, InMemoryFs, MountableFs};
use std::path::Path;
#[tokio::test]
async fn security_audit_mountable_rejects_control_chars() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);
let bad_path = Path::new("/tmp/file\x01name");
let result = mountable.write_file(bad_path, b"payload").await;
assert!(
result.is_err(),
"MountableFs must reject paths with control characters"
);
}
#[tokio::test]
async fn security_audit_mountable_validates_symlink_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);
let bad_link = Path::new("/tmp/link\x02name");
let result = mountable.symlink(Path::new("/target"), bad_link).await;
assert!(result.is_err(), "MountableFs must validate symlink paths");
}
}
mod collect_dirs_depth_limit {
use super::*;
#[tokio::test]
async fn security_audit_glob_star_star_respects_depth() {
let limits = ExecutionLimits::new()
.max_commands(200)
.timeout(Duration::from_secs(10));
let mut bash = Bash::builder().limits(limits).build();
let result = bash
.exec("mkdir -p /tmp/globtest/sub && touch /tmp/globtest/sub/file.txt && echo ok")
.await
.unwrap();
assert_eq!(result.stdout.trim(), "ok");
let result = bash.exec("echo /tmp/globtest/**").await;
assert!(
result.is_ok(),
"** glob must complete without stack overflow"
);
}
}
mod parse_word_string_limits {
use super::*;
#[tokio::test]
async fn security_audit_word_parse_uses_configured_limits() {
let limits = ExecutionLimits::new()
.max_ast_depth(5)
.timeout(Duration::from_secs(5));
let mut bash = Bash::builder().limits(limits).build();
let result = bash.exec("x=hello; echo ${x:-world}").await.unwrap();
assert_eq!(result.stdout.trim(), "hello");
}
}