Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions comparison-bin/tests/differential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
25 changes: 14 additions & 11 deletions libinjectionrs/src/sqli/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
14 changes: 8 additions & 6 deletions libinjectionrs/src/sqli/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
18 changes: 14 additions & 4 deletions libinjectionrs/src/tests/test_folding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions libinjectionrs/src/tests/test_tokens_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading