use crate::{PathBoundary, StrictPathError};
#[test]
fn test_overlong_utf8_encoding_is_literal() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let overlong_payloads = [
"%c0%ae%c0%ae/%c0%af", "%c0%ae%c0%ae%c0%afetc", "%e0%80%ae%e0%80%ae/", "%c1%9c", "%c0%2e%c0%2e", ];
for attack_input in overlong_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Overlong encoding '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {
}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_html_entity_encoding_is_literal() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let html_entity_payloads = [
"../etc/passwd", "../secret", "../../", ];
for attack_input in html_entity_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"HTML entity '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_hex_escape_sequences_are_literal() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let hex_payloads = [
"\\x2e\\x2e\\x2f", "\\x2e\\x2e/secret.txt", "\\x5c\\x2e\\x2e", ];
for attack_input in hex_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Hex escape '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_zero_width_characters_cannot_escape() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let zero_width_payloads = [
"safe\u{200B}file.txt", "\u{200C}../..\u{200C}", "\u{200B}../\u{200B}secret", "\u{FEFF}file.txt", ];
for attack_input in zero_width_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Zero-width char in '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_code_page_homoglyphs_cannot_escape() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let homoglyph_payloads = [
"..\u{00A5}..\\etc", "..\u{20A9}..\\secret", "..\u{00B4}etc/passwd", ];
for attack_input in homoglyph_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Code page homoglyph in '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_protocol_schemes_cannot_escape() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let protocol_payloads = [
"http://evil.com/malware",
"https://evil.com/payload",
"ftp://attacker/exploit",
"data:text/html,<script>alert(1)</script>",
"jar:file:///tmp/evil.jar!/payload",
"php://filter/convert.base64-encode/resource=index",
];
for attack_input in protocol_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Protocol scheme '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_whitespace_exploitation_cannot_escape() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let whitespace_payloads = [
"..\t/secret", ".. /etc/passwd", ".\t./../../etc", "safe\x0Bfile.txt", "safe\x0Cfile.txt", ];
for attack_input in whitespace_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Whitespace exploit '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_tilde_expansion_is_literal() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let tilde_payloads = [
"~/secret.txt",
"~root/.ssh/authorized_keys",
"~admin/../../../etc/shadow",
];
for attack_input in tilde_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Tilde expansion '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_env_var_syntax_is_literal() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let env_payloads = [
"$USER/.ssh/id_rsa",
"${HOME}/secret",
"%USERPROFILE%\\Desktop",
"%SystemRoot%\\System32\\config\\SAM",
];
for attack_input in env_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Env var '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}
#[test]
fn test_multi_layer_encoding_attacks() {
let temp = tempfile::tempdir().unwrap();
let boundary: PathBoundary = PathBoundary::try_new(temp.path()).unwrap();
let multi_encoding_payloads = [
"%25252e%25252e%25252f", "..%c0%af..%c0%afetc/passwd", "..%252f..%252f..%252fetc/shadow", "%2e%2e%5c%2e%2e%5cetc%5cpasswd", "..%u002f..%u002fetc/passwd", "\u{FF0E}\u{FF0E}/\u{FF0E}\u{FF0E}/etc", ];
for attack_input in multi_encoding_payloads {
match boundary.strict_join(attack_input) {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(boundary.interop_path()),
"Multi-encoding attack '{attack_input}' escaped boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {}
Err(other) => panic!("Unexpected error for '{attack_input}': {other:?}"),
}
}
}