-
Notifications
You must be signed in to change notification settings - Fork 13
fix: prevent null byte truncation bypass in SQL injection detection #410
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
base: main
Are you sure you want to change the base?
Changes from all commits
675aab0
8df44b7
61696f0
5925d46
1dad4a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,10 +27,10 @@ zval* Server::GetServerVar() { | |
| std::string Server::GetVar(const char* var) { | ||
| GET_SERVER_VAR(); | ||
| zval* data = zend_hash_str_find(Z_ARRVAL_P(serverVars), var, strlen(var)); | ||
| if (!data) { | ||
| if (!data || Z_TYPE_P(data) != IS_STRING) { | ||
| return ""; | ||
| } | ||
| return Z_STRVAL_P(data); | ||
| return std::string(Z_STRVAL_P(data), Z_STRLEN_P(data)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GetVar now constructs and returns a std::string copy for each call, causing a heap allocation and memory copy per call; consider returning a non-owning view or deferring copies to callers. Details✨ AI Reasoning 🔧 How do I fix it? Reply |
||
| } | ||
|
|
||
| // Return the method from the query param _method (_GET["_method"]) | ||
|
|
@@ -48,7 +48,7 @@ std::string Server::GetMethodFromQuery() { | |
| if (Z_TYPE_P(query_method) != IS_STRING) { | ||
| return ""; | ||
| } | ||
| std::string query_method_str = Z_STRVAL_P(query_method); | ||
| std::string query_method_str(Z_STRVAL_P(query_method), Z_STRLEN_P(query_method)); | ||
| return ToUppercase(query_method_str); | ||
| } | ||
|
|
||
|
|
@@ -160,9 +160,9 @@ std::string Server::GetHeaders() { | |
| zval* val; | ||
| ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(serverVars), key, val) { | ||
| if (key && Z_TYPE_P(val) == IS_STRING) { | ||
| std::string header_name(ZSTR_VAL(key)); | ||
| std::string header_name(ZSTR_VAL(key), ZSTR_LEN(key)); | ||
| std::string http_header_key; | ||
| std::string http_header_value(Z_STRVAL_P(val)); | ||
| std::string http_header_value(Z_STRVAL_P(val), Z_STRLEN_P(val)); | ||
|
|
||
| if (header_name.find("HTTP_") == 0) { | ||
| http_header_key = header_name.substr(5); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "AIKIDO_BLOCK": "1", | ||
| "AIKIDO_LOCALHOST_ALLOWED_BY_DEFAULT": "0", | ||
| "AIKIDO_FEATURE_COLLECT_API_SCHEMA": "1" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "type": "detected_attack", | ||
| "request": { | ||
| "headers": { | ||
| "content_type": [ | ||
| "application/json" | ||
| ] | ||
| }, | ||
| "method": "POST", | ||
| "body": "{\"userId\": \"1\\u0000 OR 1=1\"}", | ||
| "source": "php", | ||
| "route": "/testDetection" | ||
| }, | ||
| "attack": { | ||
| "kind": "sql_injection", | ||
| "operation": "mysqli->query", | ||
| "module": "mysqli", | ||
| "blocked": true, | ||
| "source": "body", | ||
| "path": ".userId", | ||
| "payload": "1\u0000 OR 1=1", | ||
| "metadata": { | ||
| "dialect": "mysql", | ||
| "sql": "SELECT * FROM users WHERE id = 1\u0000 OR 1=1" | ||
| }, | ||
| "user": { | ||
| "id": "12345", | ||
| "name": "Tudor" | ||
| } | ||
| }, | ||
| "agent": { | ||
| "library": "firewall-php" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| <?php | ||
|
|
||
| \aikido\set_user("12345", "Tudor"); | ||
|
|
||
| $mysqli = new mysqli("127.0.0.1", "root", "pwd", "db"); | ||
|
|
||
| if ($mysqli->connect_error) { | ||
| die("Connection failed: " . $mysqli->connect_error); | ||
| } | ||
|
|
||
| $createTable = " | ||
| CREATE TEMPORARY TABLE IF NOT EXISTS users ( | ||
| id INT AUTO_INCREMENT PRIMARY KEY, | ||
| name VARCHAR(255), | ||
| email VARCHAR(255) | ||
| ) | ||
| "; | ||
| $mysqli->query($createTable); | ||
|
|
||
| $mysqli->query("INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com')"); | ||
|
|
||
| $requestBody = file_get_contents('php://input'); | ||
| $data = json_decode($requestBody, true); | ||
|
|
||
| $userId = $data['userId']; | ||
| $result = $mysqli->query("SELECT * FROM users WHERE id = " . $userId); | ||
|
|
||
| echo "Query executed!"; | ||
|
|
||
| $mysqli->close(); | ||
|
|
||
| ?> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "success": true, | ||
| "serviceId": 1, | ||
| "heartbeatIntervalInMS": 600000, | ||
| "endpoints": [], | ||
| "blockedUserIds": [], | ||
| "allowedIPAddresses": [], | ||
| "receivedAnyStats": true, | ||
| "block": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import requests | ||
| import time | ||
| import sys | ||
| from testlib import * | ||
|
|
||
| ''' | ||
| Test that SQL injection payloads containing embedded null bytes (\x00) are | ||
| still detected and blocked. Before the fix, null bytes caused truncation in | ||
| the C++/Go context pipeline, letting the detection logic see only the benign | ||
| prefix (e.g. "1") while MySQL executed the full payload. | ||
| ''' | ||
|
|
||
| def check_null_byte_sqli_blocked(response_code, response_body, event_id, expected_json): | ||
| response = php_server_post("/testDetection", {"userId": "1\x00 OR 1=1"}) | ||
| assert_response_code_is(response, response_code) | ||
| assert_response_body_contains(response, response_body) | ||
|
|
||
| mock_server_wait_for_new_events(5) | ||
|
|
||
| events = mock_server_get_events() | ||
| assert_events_length_is(events, event_id + 1) | ||
| assert_started_event_is_valid(events[0]) | ||
| assert_event_contains_subset_file(events[event_id], expected_json) | ||
|
|
||
| def run_test(): | ||
| check_null_byte_sqli_blocked(500, "", 1, "expect_detection_blocked.json") | ||
|
|
||
| if __name__ == "__main__": | ||
| load_test_args() | ||
| run_test() |
Uh oh!
There was an error while loading. Please reload this page.