Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JS injection detection #43

Merged
merged 17 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ name = "zen_internals"
crate-type = ["cdylib"]

[dependencies]
oxc = "0.38.0"
regex = "1.10.6"
sqlparser = { git = "https://github.com/AikidoSec/datafusion-sqlparser-rs.git", branch = "main" }
url = "2.5.2"
Expand Down
29 changes: 29 additions & 0 deletions src/ffi_bindings/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::js_injection::detect_js_injection::detect_js_injection_str;
use crate::shell_injection::detect_shell_injection::detect_shell_injection_stringified;
use crate::sql_injection::detect_sql_injection::detect_sql_injection_str;
use std::ffi::CStr;
Expand Down Expand Up @@ -59,3 +60,31 @@ pub extern "C" fn detect_sql_injection(
})
.unwrap_or(2);
}

#[no_mangle]
pub extern "C" fn detect_js_injection(
code: *const c_char,
userinput: *const c_char,
sourcetype: c_int,
) -> c_int {
// Returns an integer value, representing a boolean (1 = true, 0 = false, 2 = error)
return panic::catch_unwind(|| {
// Check if the pointers are null
if code.is_null() || userinput.is_null() {
return 2;
}

let code_bytes = unsafe { CStr::from_ptr(code).to_bytes() };
let userinput_bytes = unsafe { CStr::from_ptr(userinput).to_bytes() };

let code_str = str::from_utf8(code_bytes).unwrap();
let userinput_str = str::from_utf8(userinput_bytes).unwrap();

if detect_js_injection_str(code_str, userinput_str, sourcetype) {
return 1;
}

return 0;
})
.unwrap_or(2);
}
hansott marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion src/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@

pub mod diff_in_vec_len;
87 changes: 87 additions & 0 deletions src/js_injection/detect_js_injection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use super::have_comments_changed::have_comments_changed;
use super::have_statements_changed::have_statements_changed;
use super::helpers::select_sourcetype_based_on_enum::select_sourcetype_based_on_enum;
use super::is_safe_js_input::is_safe_js_input;
use oxc::allocator::Allocator;
use oxc::parser::{ParseOptions, Parser};
use oxc::span::SourceType;

pub fn detect_js_injection_str(code: &str, userinput: &str, sourcetype: i32) -> bool {
if userinput.len() <= 1 {
// We assume that a single character cannot be an injection.
return false;
}

if userinput.len() > code.len() {
// If the user input is longer than the code, it's not an injection.
return false;
}

if !code.contains(userinput) {
// If the query does not contain the user input, it's not an injection.
return false;
}

let allocator = Allocator::default();
let source_type: SourceType = select_sourcetype_based_on_enum(sourcetype);

if is_safe_js_input(userinput, &allocator, source_type) {
// Ignore some non dangerous inputs, e.g. math
return false;
}

let parser_result = Parser::new(&allocator, &code, source_type)
.with_options(ParseOptions {
allow_return_outside_function: true,
..ParseOptions::default()
})
.parse();

if parser_result.panicked || parser_result.errors.len() > 0 {
return false;
}

let safe_replace_str = "a".repeat(userinput.len());
let mut code_without_input: String = code.replace(userinput, &safe_replace_str);

let mut parser_result_without_input = Parser::new(&allocator, &code_without_input, source_type)
.with_options(ParseOptions {
allow_return_outside_function: true,
..ParseOptions::default()
})
.parse();

if parser_result_without_input.panicked || parser_result_without_input.errors.len() > 0 {
// Try to parse by replacing the user input with a empty string.
code_without_input = code.replace(userinput, "");

parser_result_without_input = Parser::new(&allocator, &code_without_input, source_type)
.with_options(ParseOptions {
allow_return_outside_function: true,
..ParseOptions::default()
})
.parse();

if parser_result_without_input.panicked || parser_result_without_input.errors.len() > 0 {
return false;
}
}

if have_comments_changed(
&parser_result.program.comments,
&parser_result_without_input.program.comments,
) {
// If the number of comments is different, it's an injection.
return true;
}

if have_statements_changed(
&parser_result.program,
&parser_result_without_input.program,
&allocator,
) {
return true;
}

return false;
}
253 changes: 253 additions & 0 deletions src/js_injection/detect_js_injection_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
macro_rules! is_injection {
($code:expr, $input:expr, $sourcetype:expr) => {
assert!(detect_js_injection_str(
&$code.to_lowercase(),
&$input.to_lowercase(),
$sourcetype
))
};
}
macro_rules! not_injection {
($code:expr, $input:expr, $sourcetype:expr) => {
assert!(!detect_js_injection_str(
&$code.to_lowercase(),
&$input.to_lowercase(),
$sourcetype
))
};
}

#[cfg(test)]
mod tests {
use crate::js_injection::detect_js_injection::detect_js_injection_str;

#[test]
fn test_cjs_const() {
not_injection!("const test = 'Hello World!';", "Hello World!", 0);
is_injection!("const test = 'Hello World!'; //';", "Hello World!'; //", 0);
is_injection!(
"const test = 'Hello World!';console.log('Injected!'); //';",
"Hello World!';console.log('Injected!'); //",
0
);
is_injection!(
"const test = 'Hello World!'; console.log('injection'); // This is a comment'; // Test",
"Hello World!'; console.log('injection'); // This is a comment",
0
);
is_injection!(
"const test = 'Hello World!'; // This is a comment'; // Test",
"Hello World!'; // This is a comment",
0
);
}

#[test]
fn test_cjs_if() {
not_injection!("if (true) { return true; }", "true", 0);
not_injection!("if (1 > 5) { return true; }", "5", 0);
is_injection!(
"if(username === 'admin' || 1 === 1) { return true; } //');",
"admin' || 1 === 1) { return true; } //",
0
);
not_injection!(
"if(username === 'admin' || 1 === 1) { return true; }",
"admin",
0
);
is_injection!(
"if (username === 'admin' || 1 === 1) { return true; } //') {}",
"admin' || 1 === 1) { return true; } //",
0
);
is_injection!("if (1 > 5 || 1 === 1) { return true; }", "5 || 1 === 1", 0);
}

#[test]

fn mongodb_js() {
not_injection!("this.name === 'a' && sleep(2000) && 'b'", "a", 0);
not_injection!("this.group === 1", "1", 0);
is_injection!(
"this.name === 'a' && sleep(2000) && 'b'",
"a' && sleep(2000) && 'b",
0
);
is_injection!("const test = this.group === 1 || 1 === 1;", "1 || 1 ===", 0);
}

#[test]
fn test_cjs_function() {
not_injection!("function test() { return 'Hello'; }", "Hello", 0);
not_injection!("test(\"arg1\", 0, true);", "arg1", 0);
is_injection!(
"function test() { return 'Hello'; } //';}",
"Hello'; } //",
0
);
is_injection!(
"test(\"arg1\", 12, true); // \", 0, true);",
"arg1\", 12, true); // ",
0
);
}

#[test]
fn test_cjs_object() {
not_injection!("const obj = { test: 'value', isAdmin: true };", "value", 0);
is_injection!(
"const obj = { test: 'value', isAdmin: true }; //'};",
"value', isAdmin: true }; //",
0
);
not_injection!("const obj = [1, 2, 3];", "1", 0);
not_injection!("const obj = { test: [1, 2, 3] };", "1", 0);
not_injection!("const obj = { test: [1, 4, 2, 3] };", "1, 4", 0);
is_injection!(
"const obj = { test: [1, 4], test2: [2, 3] };",
"1, 4], test2: [2, 3",
0
);
}

#[test]
fn test_ts_code() {
not_injection!("const obj: string = 'Hello World!';", "Hello World!", 1);
is_injection!(
"const obj: string = 'Hello World!'; console.log('Injected!'); //';",
"Hello World!'; console.log('Injected!'); //",
1
);
not_injection!("function test(): string { return 'Hello'; }", "Hello", 1);
is_injection!(
"function test(): string { return 'Hello'; } //';}",
"Hello'; } //",
1
);
// Not an injection because code can not be parsed as JavaScript.
not_injection!(
"function test(): string { return 'Hello'; } //';}",
"Hello'; } //",
0
);
}

#[test]
fn test_import() {
for sourcetype in 0..5 {
not_injection!(
"import { test } from 'module'; test('Hello');",
"Hello",
sourcetype
);
is_injection!(
"import { test } from 'module'; test('Hello'); console.log('Injected!'); //');",
"Hello'); console.log('Injected!'); //",
sourcetype
);
}
}

#[test]
fn test_no_js_injection() {
not_injection!("Hello World!", "Hello World!", 0);
not_injection!("", "", 0);
not_injection!("", "Hello World!", 0);
not_injection!("Hello World!", "", 0);
not_injection!("const test = 123;", "123", 0);
not_injection!("// Reason: Test", "Test", 0);
}

#[test]
fn invalid_js_without_userinput() {
// In this case the JS code will be invalid without user input, that's why it's not detected as an injection.
// The code author wrote bad code expecting the user input to contain js code.
not_injection!(
"const test = 'Hello World!'; console.log('Injected!');",
"Hello World!'; console.log('Injected!');",
0
);
}

#[test]
fn test_js_allow_math() {
not_injection!("const test = 1 + 2;", "1 + 2", 0);
not_injection!("const test = 5 / 6 + 2;", "5 / 6 + 2", 0);
not_injection!("const test = 5 % 2 + 5.6;", "5 % 2 + 5.6", 0);
}

#[test]
fn test_js_real_cve() {
// CVE-2024-21511
is_injection!(
"packet.readDateTimeString('abc'); process.exit(1); // ');",
"abc'); process.exit(1); //",
0
);
// GHSA-q849-wxrc-vqrp
is_injection!(
"const o = {}; o['x']= pt[0]; o['y']=1; process.exit(); return o;",
"1; process.exit()",
0
);
not_injection!("const o = {}; o['x']= pt[0]; o['y']=2; return o;", "2", 2);
// CVE-2021-21278
is_injection!(
"const window={}; alert('!'); return window.__NUXT__",
"alert('!');",
0
);
// CVE-2023-34232
is_injection!(
"(\"[]\"); fetch('https://example.com/'); // \");",
"[]\"); fetch('https://example.com/'); //",
0
);
// CVE-2023-1283
is_injection!(
"(() => {
console.log(\"[+] Qwik RCE demo, by ohb00.\")
process.binding('spawn_sync').spawn({
file: 'C:\\Windows\\System32\\cmd.exe',
args: [
'cmd', '/c', 'calc.exe'
],
stdio: [
{type:'pipe',readable:!0,writable:!1},
{type:'pipe',readable:!1,writable:!0},
{type:'pipe',readable:!1,writable:!0}

]
})
return {}
})()",
"(() => {
console.log(\"[+] Qwik RCE demo, by ohb00.\")
process.binding('spawn_sync').spawn({
file: 'C:\\Windows\\System32\\cmd.exe',
args: [
'cmd', '/c', 'calc.exe'
],
stdio: [
{type:'pipe',readable:!0,writable:!1},
{type:'pipe',readable:!1,writable:!0},
{type:'pipe',readable:!1,writable:!0}

]
})
return {}
})()",
0
)
}

#[test]
fn test_js_return_without_function() {
is_injection!(
"return 'test'; console.log('injection'); //';",
"test'; console.log('injection'); //",
0
);
}
}
Loading
Loading