diff --git a/comparison-bin/tests/differential.rs b/comparison-bin/tests/differential.rs index af1227e..664ea28 100644 --- a/comparison-bin/tests/differential.rs +++ b/comparison-bin/tests/differential.rs @@ -420,3 +420,24 @@ fn nul_as_whitespace_in_html5_matches_the_c_library() { ); assert!(!c_xss(input), "C treats this as safe, and so must the port"); } + +/// Guards the variable token value: C stores the name without the leading `@` +/// (the `@` count lives in a separate field), so the function fold that matches +/// a name like `PASSWORD` sees `pasSword`, not `@pasSword`. The corpus has no +/// `@`-variable named like a function followed by `(`, so the fuzzer found it. +#[test] +fn at_variable_named_like_a_function_matches_the_c_library() { + // `@pasSword(` : C types the variable as a function (fingerprint `f`), so + // this folds to `f(f(1`, a blacklisted pattern. The port must agree. + let input: &[u8] = b"@pasSword(pasSword(2"; + let (c_is, c_fp) = c_sqli(input); + let rust = libinjectionrs::detect_sqli(input); + let rust_fp = rust.fingerprint.as_ref().map(|f| f.to_string()).unwrap_or_default(); + assert_eq!( + (rust.is_injection(), rust_fp.as_str()), + (c_is, c_fp.as_str()), + "@-variable named like a function diverges from the C library" + ); + assert_eq!(c_fp, "f(f(1", "C folds the variable to a function"); + assert!(c_is, "C flags this injection, and so must the port"); +} diff --git a/libinjectionrs/src/sqli/tests.rs b/libinjectionrs/src/sqli/tests.rs index 7c0c9a9..798ed25 100644 --- a/libinjectionrs/src/sqli/tests.rs +++ b/libinjectionrs/src/sqli/tests.rs @@ -101,25 +101,28 @@ mod tests { fn test_variable_token_symbols_preserved() { use crate::sqli::tokenizer::{SqliTokenizer, TokenType}; + // C stores the name without '@'; the '@' count lives in `count`, and + // the printed form reconstructs the '@' prefix from it. + // (input, expected value without '@', expected count) let test_cases = vec![ - ("@", "@"), - ("@@", "@@"), - ("@version", "@version"), - ("@@version", "@@version"), + ("@", "", 1i32), + ("@@", "", 2i32), + ("@version", "version", 1i32), + ("@@version", "version", 2i32), ]; - - for (input_str, expected_value) in test_cases { + + for (input_str, expected_value, expected_count) in test_cases { let input = input_str.as_bytes(); let flags = SqliFlags::new(0); let mut tokenizer = SqliTokenizer::new(input, flags); - + if let Some(token) = tokenizer.next_token() { - assert_eq!(token.token_type, TokenType::Variable, + assert_eq!(token.token_type, TokenType::Variable, "Token type should be Variable for input '{}'", input_str); assert_eq!(token.value_as_str(), expected_value, - "Token value should preserve @ symbols for input '{}'", input_str); - assert_eq!(token.pos, 0, - "Token position should start at 0 for input '{}'", input_str); + "Token value should be the name without '@' for input '{}'", input_str); + assert_eq!(token.count, expected_count, + "Token count should equal the number of '@' for input '{}'", input_str); } else { panic!("No token found for input '{}'", input_str); } diff --git a/libinjectionrs/src/sqli/tokenizer.rs b/libinjectionrs/src/sqli/tokenizer.rs index f138fd6..e16ecb8 100644 --- a/libinjectionrs/src/sqli/tokenizer.rs +++ b/libinjectionrs/src/sqli/tokenizer.rs @@ -1018,16 +1018,18 @@ impl<'a> SqliTokenizer<'a> { end_pos += 1; } + // C stores the value starting after the '@' symbols (cs + pos), so the + // token value is the name without the leading '@'. The @ count lives in + // `count`. This lets the PASSWORD/USER/... function fold match a name + // like `@pasSword`. if end_pos == new_pos { // Empty variable name (just @ or @@ symbols) - // Store the @ symbols like C implementation - let var_slice = &self.input[self.pos..new_pos]; // Include @ symbols - self.current.assign(TYPE_VARIABLE, self.pos, new_pos - self.pos, var_slice); + let var_slice = &self.input[new_pos..new_pos]; + self.current.assign(TYPE_VARIABLE, new_pos, 0, var_slice); new_pos } else { - // Non-empty variable - store the @ symbols + name like C - let var_slice = &self.input[self.pos..end_pos]; // Include @ symbols - self.current.assign(TYPE_VARIABLE, self.pos, end_pos - self.pos, var_slice); + let var_slice = &self.input[new_pos..end_pos]; + self.current.assign(TYPE_VARIABLE, new_pos, end_pos - new_pos, var_slice); end_pos } } diff --git a/libinjectionrs/src/tests/test_folding.rs b/libinjectionrs/src/tests/test_folding.rs index ba0fe4d..4749c5b 100644 --- a/libinjectionrs/src/tests/test_folding.rs +++ b/libinjectionrs/src/tests/test_folding.rs @@ -12,10 +12,20 @@ use std::path::Path; fn format_token_for_c_compatibility(token: &Token) -> String { match token.token_type { TokenType::Variable => { - // Rust tokenizer already includes @ symbols in the token value, unlike C - // C stores variable name without @ and adds them in print_var based on count - // Rust stores the full @variable string, so just return it as-is - token.value_as_str().to_string() + // C's print_var prepends `count` '@' then prints the string form + // (str_open + value + str_close); the value has no '@'. + let mut result = String::new(); + for _ in 0..token.count { + result.push('@'); + } + if token.str_open != 0 { + result.push(token.str_open as char); + } + result.push_str(token.value_as_str()); + if token.str_close != 0 { + result.push(token.str_close as char); + } + result } TokenType::String => { // Reconstruct string quotes like C's print_string function diff --git a/libinjectionrs/src/tests/test_tokens_files.rs b/libinjectionrs/src/tests/test_tokens_files.rs index 1b13abe..a03862c 100644 --- a/libinjectionrs/src/tests/test_tokens_files.rs +++ b/libinjectionrs/src/tests/test_tokens_files.rs @@ -162,10 +162,14 @@ fn format_variable_token(token: &Token) -> String { result.push(token.str_close as char); } } else { - // Simple case: @var -> value="@var" (includes @ symbols already) + // Simple case: the value is the name without '@'; C's testdriver + // prepends `count` '@' symbols to reconstruct the source form. + for _ in 0..token.count { + result.push('@'); + } result.push_str(token.value_as_str()); } - + result }