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 15 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ Cargo.lock
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
pkg/

# macOS files
.DS_Store
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
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Zen Internals library.

Zen Internals is a library that can be used via FFI in different languages. Contains algorithms to detect:
- Shell Injections (WIP)
- SQL Injections

## Python FFI Example code :
- Shell Injections (WIP)
- SQL Injections

timokoessler marked this conversation as resolved.
Show resolved Hide resolved
## Python FFI Example code :

```py
import ctypes
zen_internals = ctypes.CDLL("target/release/libzen_internals.so")
Expand All @@ -26,7 +29,7 @@ sha256sum -c zen_internals.tgz.sha256sum
tar -xzf zen_internals.tgz some-directory
```

### API
### API

#### SQL injection detection

Expand All @@ -43,3 +46,18 @@ console.log(detected); // 1
```

See [list of dialects](https://github.com/AikidoSec/zen-internals/blob/main/src/sql_injection/helpers/select_dialect_based_on_enum.rs#L18)

### JS injection detection

```js
const { wasm_detect_js_injection } = require("./some-directory/zen_internals");

const detected = wasm_detect_js_injection(
`const x = 1; console.log(x); // ;`, // code
`1; console.log(x); // ` // user input
);

console.log(detected); // 1
```

By default the function expects the input to be JavaScript code (CJS or ESM), but e.g. TypeScript is supported as well. Simply pass the corrosponding [source type number](https://github.com/AikidoSec/zen-internals/blob/main/src/js_injection/helpers/select_sourcetype_based_on_enum.rs) as third argument.
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;
}
Loading
Loading