diff --git a/appsec/src/extension/commands/request_init.c b/appsec/src/extension/commands/request_init.c index 3205adf1b2e..3baed1e143c 100644 --- a/appsec/src/extension/commands/request_init.c +++ b/appsec/src/extension/commands/request_init.c @@ -19,7 +19,6 @@ #include "../string_helpers.h" #include "request_init.h" #include -#include static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull ctx); static void _init_autoglobals(void); @@ -31,6 +30,8 @@ static void _pack_files_field_names( mpack_writer_t *nonnull w, const zend_array *nonnull files); static void _pack_path_params( mpack_writer_t *nonnull w, const zend_string *nullable uri_raw); +static void _pack_request_body(mpack_writer_t *nonnull w, + struct req_info_init *nonnull ctx, const zend_array *nonnull server); static const dd_command_spec _spec = { .name = "request_init", @@ -101,8 +102,7 @@ static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull _ctx) // 6. dd_mpack_write_lstr(w, "server.request.body"); - dd_mpack_write_array(w, dd_get_superglob_or_equiv(ZEND_STRL("_POST"), - TRACK_VARS_POST, ctx->superglob_equiv)); + _pack_request_body(w, ctx, server); // 7. const zend_array *nonnull files = dd_get_superglob_or_equiv( @@ -123,12 +123,9 @@ static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull _ctx) dd_mpack_write_nullable_zstr(w, ctx->req_info.client_ip); // 11. - if (send_raw_body && !ctx->superglob_equiv) { + if (send_raw_body && ctx->entity) { dd_mpack_write_lstr(w, "server.request.body.raw"); - zend_string *nonnull req_body = - dd_request_body_buffered(get_DD_APPSEC_MAX_BODY_BUFF_SIZE()); - dd_mpack_write_zstr(w, req_body); - zend_string_release(req_body); + dd_mpack_write_zstr(w, ctx->entity); } mpack_finish_map(w); @@ -175,7 +172,13 @@ static void _pack_headers( continue; } - if (_is_relevant_header(key)) { + if (zend_string_equals_literal(key, "CONTENT_TYPE")) { + dd_mpack_write_lstr(w, "content-type"); + dd_mpack_write_zval(w, val); + } else if (zend_string_equals_literal(key, "CONTENT_LENGTH")) { + dd_mpack_write_lstr(w, "content-length"); + dd_mpack_write_zval(w, val); + } else if (_is_relevant_header(key)) { zend_string *transf_header_name = _transform_header_name(key); dd_mpack_write_zstr(w, transf_header_name); zend_string_efree(transf_header_name); @@ -274,3 +277,31 @@ static void _pack_path_params( efree(uri_work_zstr); mpack_complete_array(w); } + +static void _pack_request_body(mpack_writer_t *nonnull w, + struct req_info_init *nonnull ctx, const zend_array *nonnull server) +{ + const zend_array *post = dd_get_superglob_or_equiv( + ZEND_STRL("_POST"), TRACK_VARS_POST, ctx->superglob_equiv); + if (zend_hash_num_elements(post) != 0) { + dd_mpack_write_array(w, post); + } else { + bool written = false; + if (ctx->entity) { + zend_string *ct = + dd_php_get_string_elem_cstr(server, ZEND_STRL("CONTENT_TYPE")); + if (ct) { + zval body_zv = dd_entity_body_convert( + ZSTR_VAL(ct), ZSTR_LEN(ct), ctx->entity); + if (Z_TYPE(body_zv) != IS_NULL) { + dd_mpack_write_zval(w, &body_zv); + zval_ptr_dtor(&body_zv); + written = true; + } + } + } + if (!written) { + dd_mpack_write_array(w, &zend_empty_array); + } + } +} diff --git a/appsec/src/extension/commands/request_init.h b/appsec/src/extension/commands/request_init.h index b3f728354c3..17600709068 100644 --- a/appsec/src/extension/commands/request_init.h +++ b/appsec/src/extension/commands/request_init.h @@ -11,6 +11,7 @@ struct req_info_init { struct req_info req_info; zend_array *nullable superglob_equiv; + zend_string *nullable entity; }; dd_result dd_request_init( dd_conn *nonnull conn, struct req_info_init *nonnull ctx); diff --git a/appsec/src/extension/commands/request_shutdown.c b/appsec/src/extension/commands/request_shutdown.c index 9508b5c307a..a475046c469 100644 --- a/appsec/src/extension/commands/request_shutdown.c +++ b/appsec/src/extension/commands/request_shutdown.c @@ -7,14 +7,12 @@ #include "request_shutdown.h" #include "../commands_helpers.h" #include "../ddappsec.h" +#include "../entity_body.h" #include "../msgpack_helpers.h" #include "../php_compat.h" #include "../php_objects.h" #include "../string_helpers.h" -#include "request_shutdown_arginfo.h" -#include "zend_exceptions.h" #include -#include static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull ctx); static void _pack_headers_no_cookies_llist( @@ -25,9 +23,6 @@ static void _pack_headers_no_cookies_map( mpack_writer_t *nonnull w, const zend_array *nonnull headers); static const char *nullable _header_content_type_zend_array( const zend_array *nonnull hl, size_t *nonnull len); -static zval _convert_json(char *nonnull entity, size_t entity_len); -static zval _convert_xml(const char *nonnull entity, size_t entity_len, - const char *content_type, size_t content_type_len); static const dd_command_spec _spec = { .name = "request_shutdown", @@ -61,17 +56,7 @@ static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull ctx) req_info->resp_headers_arr, &ct_len); } if (ct) { - if (ct_len >= LSTRLEN("application/json") && - strncasecmp(ct, LSTRARG("application/json")) == 0) { - resp_body = _convert_json( - ZSTR_VAL(req_info->entity), ZSTR_LEN(req_info->entity)); - } else if ((ct_len >= LSTRLEN("text/xml") && - strncasecmp(ct, LSTRARG("text/xml")) == 0) || - (ct_len >= LSTRLEN("application/xml") && - strncasecmp(ct, LSTRARG("application/xml")) == 0)) { - resp_body = _convert_xml(ZSTR_VAL(req_info->entity), - ZSTR_LEN(req_info->entity), ct, ct_len); - } + resp_body = dd_entity_body_convert(ct, ct_len, req_info->entity); } } @@ -301,315 +286,3 @@ static const char *nullable _header_content_type_zend_array( return NULL; } - -static zval _convert_json(char *nonnull entity, size_t entity_len) -{ - zval zv; - ZVAL_NULL(&zv); -#define MAX_DEPTH 12 - php_json_decode_ex( - &zv, entity, entity_len, PHP_JSON_OBJECT_AS_ARRAY, MAX_DEPTH); - if (Z_TYPE(zv) == IS_NULL) { - mlog(dd_log_info, "Failed to parse JSON response body"); - zval_ptr_dtor(&zv); - } - return zv; -} - -static bool _assume_utf8(const char *ct, size_t ct_len) -{ - const char *psemi = memchr(ct, ';', ct_len); - if (!psemi) { - return true; - } - for (const char *end = ct + ct_len, *c = psemi + 1; - c < end - LSTRLEN("charset=utf-8") + 1; c++) { - if (tolower(*c) == 'c' && tolower(*(c + 1)) == 'h' && - tolower(*(c + 2)) == 'a' && tolower(*(c + 3)) == 'r' && - tolower(*(c + 4)) == 's' && tolower(*(c + 5)) == 'e' && // NOLINT - tolower(*(c + 6)) == 't') { // NOLINT - c += LSTRLEN("charset"); - for (; c < end && *c == ' '; c++) {} - if (c < end && *c == '=') { - for (c++; c < end - LSTRLEN("utf-8") && *c == ' '; c++) {} - if (tolower(*c) == 'u' && tolower(*(c + 1)) == 't' && - tolower(*(c + 2)) == 'f' && tolower(*(c + 3)) == '-' && - tolower(*(c + 4)) == '8') { - return true; - } - return false; - } - return true; - } - } - return true; -} - -static zval _convert_xml_impl(const char *nonnull entity, size_t entity_len, - const char *content_type, size_t content_type_len) -{ - static zval null_zv = {.u1.type_info = IS_NULL}; - zval function_name; - zval parser; - zval args[4]; - int is_successful; - - /* create XMLParser */ - ZVAL_STRING(&function_name, "xml_parser_create"); - is_successful = call_user_function( - CG(function_table), NULL, &function_name, &parser, 0, NULL); - zval_dtor(&function_name); - -#if PHP_VERSION_ID >= 80000 -# define XML_PARSER_TYPE IS_OBJECT -#else -# define XML_PARSER_TYPE IS_RESOURCE -#endif - if (is_successful == FAILURE || Z_TYPE(parser) != XML_PARSER_TYPE) { - mlog(dd_log_debug, "Failed to create XML parser"); - if (Z_TYPE(parser) == XML_PARSER_TYPE) { - zval_dtor(&parser); - } - return null_zv; - } - - /* disable case folding */ - zval retval; - ZVAL_STRING(&function_name, "xml_parser_set_option"); - ZVAL_COPY_VALUE(&args[0], &parser); - ZVAL_LONG(&args[1], 1 /*PHP_XML_OPTION_CASE_FOLDING*/); - ZVAL_BOOL(&args[2], 0); - is_successful = call_user_function( - CG(function_table), NULL, &function_name, &retval, 3, args); - if (is_successful == FAILURE || Z_TYPE_P(&retval) != IS_TRUE) { - mlog(dd_log_debug, "Failed to set XML parser option"); - zval_dtor(&function_name); - zval_dtor(&parser); - return null_zv; - } - - /* skip whitespace */ - ZVAL_LONG(&args[1], 4 /*PHP_XML_OPTION_SKIP_WHITE*/); - ZVAL_BOOL(&args[2], 1); - is_successful = call_user_function( - CG(function_table), NULL, &function_name, &retval, 3, args); - zval_dtor(&function_name); - if (is_successful == FAILURE || Z_TYPE_P(&retval) != IS_TRUE) { - mlog(dd_log_debug, "Failed to set XML parser option"); - zval_dtor(&parser); - return null_zv; - } - - // check if the encoding is UTF-8 - // PHP's xml_parse_into_struct does not support other encodings - // even after setting the option XML_OPTION_TARGET_ENCODING - // It never calls xmlSwitchToEncoding() - bool is_utf8 = _assume_utf8(content_type, content_type_len); - if (!is_utf8) { - mlog(dd_log_info, "Only UTF-8 is supported for XML parsing"); - zval_dtor(&parser); - return null_zv; - } - - // Call xml_parse_into_struct - ZVAL_STRING(&function_name, "xml_parse_into_struct"); - ZVAL_STRINGL(&args[1], entity, entity_len); - ZVAL_NULL(&args[2]); - ZVAL_MAKE_REF(&args[2]); - ZVAL_NULL(&args[3]); - ZVAL_MAKE_REF(&args[3]); - is_successful = call_user_function( - CG(function_table), NULL, &function_name, &retval, 4, args); - zval_dtor(&function_name); - zval_dtor(&parser); // parser = args[0] - zval_dtor(&args[1]); - zval_dtor(&args[3]); // we don't care about the index result - if (is_successful == FAILURE || Z_TYPE(args[2]) != IS_REFERENCE || - Z_TYPE_P(Z_REFVAL(args[2])) != IS_ARRAY || Z_TYPE(retval) != IS_LONG || - Z_LVAL(retval) != 1) { - mlog(dd_log_debug, "Failed to parse XML response body"); - zval_dtor(&args[2]); - return null_zv; - } - - // now transform the the result - // each tag is encoded as a singleton map: - // : {content: [...], attributes: {...}) - // text is encoded as string - zend_array *root = zend_new_array(1); - zend_array *cur = root; // non-owning - zend_array *stack; // non-owning - ALLOC_HASHTABLE(stack); - zend_hash_init(stack, 1, NULL, NULL, 0); - - zval *val_zv; - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(Z_REFVAL(args[2])), val_zv) - { - if (Z_TYPE_P(val_zv) != IS_ARRAY) { - continue; - } - zend_array *val = Z_ARRVAL_P(val_zv); - zval *tag_zv = zend_hash_str_find(val, LSTRARG("tag")); - if (!tag_zv || Z_TYPE_P(tag_zv) != IS_STRING) { - continue; - } - zval *type_zv = zend_hash_str_find(val, LSTRARG("type")); - if (!type_zv || Z_TYPE_P(type_zv) != IS_STRING) { - continue; - } - - enum { open, complete, cdata, close } type; - if (zend_string_equals_literal(Z_STR_P(type_zv), "open")) { - type = open; - } else if (zend_string_equals_literal(Z_STR_P(type_zv), "complete")) { - type = complete; - } else if (zend_string_equals_literal(Z_STR_P(type_zv), "cdata")) { - type = cdata; - } else if (zend_string_equals_literal(Z_STR_P(type_zv), "close")) { - type = close; - } else { - continue; - } - - // possible types: open, complete, cdata, close - if (type == complete || type == open) { - zval *value_zv = zend_hash_str_find(val, LSTRARG("value")); - if (value_zv && Z_TYPE_P(value_zv) != IS_STRING) { - continue; - } - zval *attr_zv = zend_hash_str_find(val, LSTRARG("attributes")); - if (attr_zv && Z_TYPE_P(attr_zv) != IS_ARRAY) { - continue; - } - - // add to cur: {: {content: [(value)], attributes: ]} - // top singleton map - zend_array *celem = zend_new_array(1); - zval celem_zv; - ZVAL_ARR(&celem_zv, celem); - zend_hash_next_index_insert(cur, &celem_zv); - - // map with keys content and attributes - zend_array *celem_val = zend_new_array(attr_zv ? 2 : 1); - { - zval celem_val_zv; - ZVAL_ARR(&celem_val_zv, celem_val); - zend_hash_add_new(celem, Z_STR_P(tag_zv), &celem_val_zv); - } - - zend_array *content = NULL; - if (type == open || value_zv) { - content = zend_new_array(1); - { - zval content_zv; - ZVAL_ARR(&content_zv, content); - zend_hash_str_add_new(celem_val, "content", - sizeof("content") - 1, &content_zv); - } - if (value_zv) { - zval_addref_p(value_zv); - zend_hash_next_index_insert(content, value_zv); - } - } - - if (attr_zv) { - zval_addref_p(attr_zv); - zend_hash_str_add_new( - celem_val, "attributes", sizeof("attributes") - 1, attr_zv); - } - - if (type == open) { - // stash cur, cur = content - zval cur_zv; - ZVAL_ARR(&cur_zv, cur); - zend_hash_next_index_insert(stack, &cur_zv); - assert(content != NULL); - cur = content; - } - } else if (type == cdata) { - zval *value_zv = zend_hash_str_find(val, LSTRARG("value")); - if (!value_zv || Z_TYPE_P(value_zv) != IS_STRING) { - continue; - } - - zval_addref_p(value_zv); - zend_hash_next_index_insert(cur, value_zv); - } else { // type == close - // stash = stash[:-1], cur=stash[-1] - uint32_t num_elems = zend_hash_num_elements(stack); - if (num_elems == 0) { - mlog(dd_log_error, "Invalid XML: too many close tags"); - break; - } - zval *cur_zv = zend_hash_index_find(stack, num_elems - 1); - if (!cur_zv) { - break; - } - zend_hash_index_del(stack, num_elems - 1); - cur = Z_ARR_P(cur_zv); - } - } - ZEND_HASH_FOREACH_END(); - - zval_dtor(&args[2]); - - zend_array_destroy(stack); - zval *ret_zvp = zend_hash_index_find(root, 0); - zval ret = null_zv; - if (ret_zvp) { - zval_addref_p(ret_zvp); - ret = *ret_zvp; - } - zend_array_destroy(root); - - return ret; -} - -static zval _convert_xml(const char *nonnull entity, size_t entity_len, - const char *content_type, size_t content_type_len) -{ - if (EG(exception)) { - return (zval){.u1.type_info = IS_NULL}; - } - - zval ret = - _convert_xml_impl(entity, entity_len, content_type, content_type_len); - if (EG(exception)) { - OBJ_RELEASE(EG(exception)); - EG(exception) = NULL; - } - return ret; -} - -PHP_FUNCTION(datadog_appsec_testing_convert_json) -{ - zend_string *entity; - ZEND_PARSE_PARAMETERS_START(1, 1) // NOLINT - Z_PARAM_STR(entity) - ZEND_PARSE_PARAMETERS_END(); - - zval result = _convert_json(ZSTR_VAL(entity), ZSTR_LEN(entity)); - RETURN_ZVAL(&result, 0, 0); -} - -PHP_FUNCTION(datadog_appsec_testing_convert_xml) -{ - zend_string *entity; - zend_string *content_type; - ZEND_PARSE_PARAMETERS_START(2, 2) // NOLINT - Z_PARAM_STR(entity) - Z_PARAM_STR(content_type) - ZEND_PARSE_PARAMETERS_END(); - - zval result = _convert_xml(ZSTR_VAL(entity), ZSTR_LEN(entity), - ZSTR_VAL(content_type), ZSTR_LEN(content_type)); - - RETURN_ZVAL(&result, 0, 0); -} - -void dd_request_shutdown_startup() -{ - if (get_global_DD_APPSEC_TESTING()) { - dd_phpobj_reg_funcs(ext_functions); - } -} diff --git a/appsec/src/extension/commands/request_shutdown.h b/appsec/src/extension/commands/request_shutdown.h index 560e4e25411..a87fa58ca18 100644 --- a/appsec/src/extension/commands/request_shutdown.h +++ b/appsec/src/extension/commands/request_shutdown.h @@ -24,6 +24,5 @@ struct req_shutdown_info { zend_string *nullable entity; }; -void dd_request_shutdown_startup(void); dd_result dd_request_shutdown( dd_conn *nonnull conn, struct req_shutdown_info *nonnull req_info); diff --git a/appsec/src/extension/ddappsec.c b/appsec/src/extension/ddappsec.c index 7a99411a028..25fa57777e9 100644 --- a/appsec/src/extension/ddappsec.c +++ b/appsec/src/extension/ddappsec.c @@ -213,7 +213,6 @@ static PHP_MINIT_FUNCTION(ddappsec) dd_tags_startup(); dd_ip_extraction_startup(); dd_entity_body_startup(); - dd_request_shutdown_startup(); return SUCCESS; } diff --git a/appsec/src/extension/ddtrace.h b/appsec/src/extension/ddtrace.h index fecfdae0650..9f7b2941927 100644 --- a/appsec/src/extension/ddtrace.h +++ b/appsec/src/extension/ddtrace.h @@ -63,7 +63,7 @@ struct _ddtrace_user_req_listeners { int priority; zend_array *nullable (*nonnull start_user_req)( ddtrace_user_req_listeners *nonnull self, zend_object *nonnull span, - zend_array *nonnull variables); + zend_array *nonnull variables, zval *nullable rbe_zv); zend_array *nullable(*nonnull response_committed)( ddtrace_user_req_listeners *nonnull self, zend_object *nonnull span, int status, zend_array *nonnull headers, zval *nullable entity); diff --git a/appsec/src/extension/entity_body.c b/appsec/src/extension/entity_body.c index 8bf2e7b3939..4afe0613839 100644 --- a/appsec/src/extension/entity_body.c +++ b/appsec/src/extension/entity_body.c @@ -5,8 +5,12 @@ // (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. #include "entity_body.h" #include "ddappsec.h" +#include "entity_body_arginfo.h" #include "logging.h" +#include "php_objects.h" +#include "string_helpers.h" #include +#include #include static typeof(zend_write(NULL, 0)) _dd_save_output_zend_write( @@ -15,6 +19,10 @@ static typeof(zend_write(NULL, 0)) _dd_save_output_zend_write( ZEND_TLS zend_string *_buffer; ZEND_TLS size_t _buffer_size; +zval _convert_json(char *nonnull entity, size_t entity_len); +zval _convert_xml(const char *nonnull entity, size_t entity_len, + const char *nonnull content_type, size_t content_type_len); + // we need to keep track of all buffers so we can free them on shutdown // this is in order to avoid having memory leaks reported static HashTable _all_buffers; @@ -28,6 +36,10 @@ void dd_entity_body_startup() orig_zend_write = zend_write; zend_write = _dd_save_output_zend_write; zend_hash_init(&_all_buffers, 0, NULL, NULL, 1); + + if (get_global_DD_APPSEC_TESTING()) { + dd_phpobj_reg_funcs(ext_functions); + } } void dd_entity_body_shutdown() @@ -123,3 +135,327 @@ zend_string *nonnull dd_request_body_buffered(size_t limit) return body_data; } + +zval dd_entity_body_convert( + const char *nonnull ct, size_t ct_len, zend_string *nonnull entity) +{ + if (ct_len >= LSTRLEN("application/json") && + strncasecmp(ct, LSTRARG("application/json")) == 0) { + return _convert_json(ZSTR_VAL(entity), ZSTR_LEN(entity)); + } + if ((ct_len >= LSTRLEN("text/xml") && + strncasecmp(ct, LSTRARG("text/xml")) == 0) || + (ct_len >= LSTRLEN("application/xml") && + strncasecmp(ct, LSTRARG("application/xml")) == 0)) { + return _convert_xml(ZSTR_VAL(entity), ZSTR_LEN(entity), ct, ct_len); + } + return (zval){.u1.type_info = IS_NULL}; +} + +zval _convert_json(char *nonnull entity, size_t entity_len) +{ + zval zv; + ZVAL_NULL(&zv); +#define MAX_DEPTH 12 + php_json_decode_ex( + &zv, entity, entity_len, PHP_JSON_OBJECT_AS_ARRAY, MAX_DEPTH); + if (Z_TYPE(zv) == IS_NULL) { + mlog(dd_log_info, "Failed to parse JSON response body"); + zval_ptr_dtor(&zv); + } + return zv; +} + +static zval _convert_xml_impl(const char *nonnull entity, size_t entity_len, + const char *content_type, size_t content_type_len); +zval _convert_xml(const char *nonnull entity, size_t entity_len, + const char *nonnull content_type, size_t content_type_len) +{ + if (EG(exception)) { + return (zval){.u1.type_info = IS_NULL}; + } + + zval ret = + _convert_xml_impl(entity, entity_len, content_type, content_type_len); + if (EG(exception)) { + OBJ_RELEASE(EG(exception)); + EG(exception) = NULL; + } + return ret; +} + +static bool _assume_utf8(const char *ct, size_t ct_len); +static zval _convert_xml_impl(const char *nonnull entity, size_t entity_len, + const char *nonnull content_type, size_t content_type_len) +{ + static zval null_zv = {.u1.type_info = IS_NULL}; + zval function_name; + zval parser; + zval args[4]; + int is_successful; + + /* create XMLParser */ + ZVAL_STRING(&function_name, "xml_parser_create"); + is_successful = call_user_function( + CG(function_table), NULL, &function_name, &parser, 0, NULL); + zval_dtor(&function_name); + +#if PHP_VERSION_ID >= 80000 +# define XML_PARSER_TYPE IS_OBJECT +#else +# define XML_PARSER_TYPE IS_RESOURCE +#endif + if (is_successful == FAILURE || Z_TYPE(parser) != XML_PARSER_TYPE) { + mlog(dd_log_debug, "Failed to create XML parser"); + if (Z_TYPE(parser) == XML_PARSER_TYPE) { + zval_dtor(&parser); + } + return null_zv; + } + + /* disable case folding */ + zval retval; + ZVAL_STRING(&function_name, "xml_parser_set_option"); + ZVAL_COPY_VALUE(&args[0], &parser); + ZVAL_LONG(&args[1], 1 /*PHP_XML_OPTION_CASE_FOLDING*/); + ZVAL_BOOL(&args[2], 0); + is_successful = call_user_function( + CG(function_table), NULL, &function_name, &retval, 3, args); + if (is_successful == FAILURE || Z_TYPE_P(&retval) != IS_TRUE) { + mlog(dd_log_debug, "Failed to set XML parser option"); + zval_dtor(&function_name); + zval_dtor(&parser); + return null_zv; + } + + /* skip whitespace */ + ZVAL_LONG(&args[1], 4 /*PHP_XML_OPTION_SKIP_WHITE*/); + ZVAL_BOOL(&args[2], 1); + is_successful = call_user_function( + CG(function_table), NULL, &function_name, &retval, 3, args); + zval_dtor(&function_name); + if (is_successful == FAILURE || Z_TYPE_P(&retval) != IS_TRUE) { + mlog(dd_log_debug, "Failed to set XML parser option"); + zval_dtor(&parser); + return null_zv; + } + + // check if the encoding is UTF-8 + // PHP's xml_parse_into_struct does not support other encodings + // even after setting the option XML_OPTION_TARGET_ENCODING + // It never calls xmlSwitchToEncoding() + bool is_utf8 = _assume_utf8(content_type, content_type_len); + if (!is_utf8) { + mlog(dd_log_info, "Only UTF-8 is supported for XML parsing"); + zval_dtor(&parser); + return null_zv; + } + + // Call xml_parse_into_struct + ZVAL_STRING(&function_name, "xml_parse_into_struct"); + ZVAL_STRINGL(&args[1], entity, entity_len); + ZVAL_NULL(&args[2]); + ZVAL_MAKE_REF(&args[2]); + ZVAL_NULL(&args[3]); + ZVAL_MAKE_REF(&args[3]); + is_successful = call_user_function( + CG(function_table), NULL, &function_name, &retval, 4, args); + zval_dtor(&function_name); + zval_dtor(&parser); // parser = args[0] + zval_dtor(&args[1]); + zval_dtor(&args[3]); // we don't care about the index result + if (is_successful == FAILURE || Z_TYPE(args[2]) != IS_REFERENCE || + Z_TYPE_P(Z_REFVAL(args[2])) != IS_ARRAY || Z_TYPE(retval) != IS_LONG || + Z_LVAL(retval) != 1) { + mlog(dd_log_debug, "Failed to parse XML response body"); + zval_dtor(&args[2]); + return null_zv; + } + + // now transform the the result + // each tag is encoded as a singleton map: + // : {content: [...], attributes: {...}) + // text is encoded as string + zend_array *root = zend_new_array(1); + zend_array *cur = root; // non-owning + zend_array *stack; // non-owning + ALLOC_HASHTABLE(stack); + zend_hash_init(stack, 1, NULL, NULL, 0); + + zval *val_zv; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(Z_REFVAL(args[2])), val_zv) + { + if (Z_TYPE_P(val_zv) != IS_ARRAY) { + continue; + } + zend_array *val = Z_ARRVAL_P(val_zv); + zval *tag_zv = zend_hash_str_find(val, LSTRARG("tag")); + if (!tag_zv || Z_TYPE_P(tag_zv) != IS_STRING) { + continue; + } + zval *type_zv = zend_hash_str_find(val, LSTRARG("type")); + if (!type_zv || Z_TYPE_P(type_zv) != IS_STRING) { + continue; + } + + enum { open, complete, cdata, close } type; + if (zend_string_equals_literal(Z_STR_P(type_zv), "open")) { + type = open; + } else if (zend_string_equals_literal(Z_STR_P(type_zv), "complete")) { + type = complete; + } else if (zend_string_equals_literal(Z_STR_P(type_zv), "cdata")) { + type = cdata; + } else if (zend_string_equals_literal(Z_STR_P(type_zv), "close")) { + type = close; + } else { + continue; + } + + // possible types: open, complete, cdata, close + if (type == complete || type == open) { + zval *value_zv = zend_hash_str_find(val, LSTRARG("value")); + if (value_zv && Z_TYPE_P(value_zv) != IS_STRING) { + continue; + } + zval *attr_zv = zend_hash_str_find(val, LSTRARG("attributes")); + if (attr_zv && Z_TYPE_P(attr_zv) != IS_ARRAY) { + continue; + } + + // add to cur: {: {content: [(value)], attributes: ]} + // top singleton map + zend_array *celem = zend_new_array(1); + zval celem_zv; + ZVAL_ARR(&celem_zv, celem); + zend_hash_next_index_insert(cur, &celem_zv); + + // map with keys content and attributes + zend_array *celem_val = zend_new_array(attr_zv ? 2 : 1); + { + zval celem_val_zv; + ZVAL_ARR(&celem_val_zv, celem_val); + zend_hash_add_new(celem, Z_STR_P(tag_zv), &celem_val_zv); + } + + zend_array *content = NULL; + if (type == open || value_zv) { + content = zend_new_array(1); + { + zval content_zv; + ZVAL_ARR(&content_zv, content); + zend_hash_str_add_new(celem_val, "content", + sizeof("content") - 1, &content_zv); + } + if (value_zv) { + zval_addref_p(value_zv); + zend_hash_next_index_insert(content, value_zv); + } + } + + if (attr_zv) { + zval_addref_p(attr_zv); + zend_hash_str_add_new( + celem_val, "attributes", sizeof("attributes") - 1, attr_zv); + } + + if (type == open) { + // stash cur, cur = content + zval cur_zv; + ZVAL_ARR(&cur_zv, cur); + zend_hash_next_index_insert(stack, &cur_zv); + assert(content != NULL); + cur = content; + } + } else if (type == cdata) { + zval *value_zv = zend_hash_str_find(val, LSTRARG("value")); + if (!value_zv || Z_TYPE_P(value_zv) != IS_STRING) { + continue; + } + + zval_addref_p(value_zv); + zend_hash_next_index_insert(cur, value_zv); + } else { // type == close + // stash = stash[:-1], cur=stash[-1] + uint32_t num_elems = zend_hash_num_elements(stack); + if (num_elems == 0) { + mlog(dd_log_error, "Invalid XML: too many close tags"); + break; + } + zval *cur_zv = zend_hash_index_find(stack, num_elems - 1); + if (!cur_zv) { + break; + } + zend_hash_index_del(stack, num_elems - 1); + cur = Z_ARR_P(cur_zv); + } + } + ZEND_HASH_FOREACH_END(); + + zval_dtor(&args[2]); + + zend_array_destroy(stack); + zval *ret_zvp = zend_hash_index_find(root, 0); + zval ret = null_zv; + if (ret_zvp) { + zval_addref_p(ret_zvp); + ret = *ret_zvp; + } + zend_array_destroy(root); + + return ret; +} + +static bool _assume_utf8(const char *ct, size_t ct_len) +{ + const char *psemi = memchr(ct, ';', ct_len); + if (!psemi) { + return true; + } + for (const char *end = ct + ct_len, *c = psemi + 1; + c < end - LSTRLEN("charset=utf-8") + 1; c++) { + if (tolower(*c) == 'c' && tolower(*(c + 1)) == 'h' && + tolower(*(c + 2)) == 'a' && tolower(*(c + 3)) == 'r' && + tolower(*(c + 4)) == 's' && tolower(*(c + 5)) == 'e' && // NOLINT + tolower(*(c + 6)) == 't') { // NOLINT + c += LSTRLEN("charset"); + for (; c < end && *c == ' '; c++) {} + if (c < end && *c == '=') { + for (c++; c < end - LSTRLEN("utf-8") && *c == ' '; c++) {} + if (tolower(*c) == 'u' && tolower(*(c + 1)) == 't' && + tolower(*(c + 2)) == 'f' && tolower(*(c + 3)) == '-' && + tolower(*(c + 4)) == '8') { + return true; + } + return false; + } + return true; + } + } + return true; +} + +PHP_FUNCTION(datadog_appsec_testing_convert_json) +{ + zend_string *entity; + ZEND_PARSE_PARAMETERS_START(1, 1) // NOLINT + Z_PARAM_STR(entity) + ZEND_PARSE_PARAMETERS_END(); + + zval result = _convert_json(ZSTR_VAL(entity), ZSTR_LEN(entity)); + RETURN_ZVAL(&result, 0, 0); +} + +PHP_FUNCTION(datadog_appsec_testing_convert_xml) +{ + zend_string *entity; + zend_string *content_type; + ZEND_PARSE_PARAMETERS_START(2, 2) // NOLINT + Z_PARAM_STR(entity) + Z_PARAM_STR(content_type) + ZEND_PARSE_PARAMETERS_END(); + + zval result = _convert_xml(ZSTR_VAL(entity), ZSTR_LEN(entity), + ZSTR_VAL(content_type), ZSTR_LEN(content_type)); + + RETURN_ZVAL(&result, 0, 0); +} diff --git a/appsec/src/extension/entity_body.h b/appsec/src/extension/entity_body.h index ea2ed8cba99..fc4ad579037 100644 --- a/appsec/src/extension/entity_body.h +++ b/appsec/src/extension/entity_body.h @@ -14,3 +14,6 @@ void dd_entity_body_shutdown(void); void dd_entity_body_activate(void); zend_string *nonnull dd_request_body_buffered(size_t limit); zend_string *nonnull dd_response_body_buffered(void); + +zval dd_entity_body_convert( + const char *nonnull ct, size_t ct_len, zend_string *nonnull entity); diff --git a/appsec/src/extension/commands/request_shutdown.stub.php b/appsec/src/extension/entity_body.stub.php similarity index 100% rename from appsec/src/extension/commands/request_shutdown.stub.php rename to appsec/src/extension/entity_body.stub.php diff --git a/appsec/src/extension/commands/request_shutdown_arginfo.h b/appsec/src/extension/entity_body_arginfo.h similarity index 100% rename from appsec/src/extension/commands/request_shutdown_arginfo.h rename to appsec/src/extension/entity_body_arginfo.h diff --git a/appsec/src/extension/request_lifecycle.c b/appsec/src/extension/request_lifecycle.c index 548a4be6353..7efdad25898 100644 --- a/appsec/src/extension/request_lifecycle.c +++ b/appsec/src/extension/request_lifecycle.c @@ -18,20 +18,19 @@ #include "request_abort.h" #include "string_helpers.h" #include "tags.h" -#include "zend_string.h" -#include "zend_types.h" #include #include #include static void _do_request_finish_php(bool ignore_verdict); -static zend_array *nullable _do_request_begin(bool user_req); +static zend_array *nullable _do_request_begin( + zval *nullable rbe_zv, bool user_req); static void _do_request_begin_php(void); static zend_array *_do_request_finish_user_req(bool ignore_verdict, zend_array *nonnull superglob_equiv, int status_code, zend_array *nullable resp_headers, zend_string *nullable entity); -static zend_array *nullable _do_request_begin_user_req(void); +static zend_array *nullable _do_request_begin_user_req(zval *nullable rbe_zv); static zend_string *nullable _extract_ip_from_autoglobal(void); static zend_string *nullable _get_entity_as_string(zval *rbe_zv); static void _set_cur_span(zend_object *nullable span); @@ -109,21 +108,39 @@ void dd_req_lifecycle_rinit(bool force) _do_request_begin_php(); } -static void _do_request_begin_php() { (void)_do_request_begin(false); } +static void _do_request_begin_php() +{ + zend_string *nonnull req_body = + dd_request_body_buffered(get_DD_APPSEC_MAX_BODY_BUFF_SIZE()); + zval req_body_zv; + ZVAL_STR(&req_body_zv, req_body); + (void)_do_request_begin(&req_body_zv, false); +} -static zend_array *nullable _do_request_begin_user_req() +static zend_array *nullable _do_request_begin_user_req(zval *nullable rbe_zv) { - return _do_request_begin(true); + if (rbe_zv) { + Z_TRY_ADDREF_P(rbe_zv); + } + return _do_request_begin(rbe_zv, true); } -static zend_array *nullable _do_request_begin(bool user_req) +static zend_array *nullable _do_request_begin( + zval *nullable rbe_zv /* needs free */, bool user_req) { dd_tags_rinit(); + zend_string *nullable rbe = NULL; + if (rbe_zv) { + rbe = _get_entity_as_string(rbe_zv); + zval_ptr_dtor(rbe_zv); + } + struct req_info_init req_info = { .req_info.root_span = dd_req_lifecycle_get_cur_span(), .req_info.client_ip = dd_req_lifecycle_get_client_ip(), .superglob_equiv = _superglob_equiv, + .entity = rbe, }; // connect/client_init @@ -132,6 +149,9 @@ static zend_array *nullable _do_request_begin(bool user_req) if (conn == NULL) { mlog_g(dd_log_debug, "No connection; skipping rest of request initialization"); + if (rbe) { + zend_string_release(rbe); + } return NULL; } @@ -148,6 +168,10 @@ static zend_array *nullable _do_request_begin(bool user_req) } } + if (rbe) { + zend_string_release(rbe); + } + // we might have been disabled by request_init if (res == dd_network) { @@ -375,7 +399,7 @@ zend_string *nullable dd_req_lifecycle_get_client_ip() static zend_array *nullable _start_user_req( ddtrace_user_req_listeners *listener, zend_object *span, - zend_array *super_global_equiv) + zend_array *super_global_equiv, zval *nullable rbe_zv) { UNUSED(listener); @@ -399,7 +423,7 @@ static zend_array *nullable _start_user_req( _set_cur_span(span); GC_TRY_ADDREF(super_global_equiv); _superglob_equiv = super_global_equiv; - return _do_request_begin_user_req(); + return _do_request_begin_user_req(rbe_zv); } static zend_array *nullable _response_commit( diff --git a/appsec/src/extension/tags.c b/appsec/src/extension/tags.c index 1202e162d17..49f98a97a15 100644 --- a/appsec/src/extension/tags.c +++ b/appsec/src/extension/tags.c @@ -34,6 +34,8 @@ #define DD_TAG_HTTP_URL "http.url" #define DD_TAG_NETWORK_CLIENT_IP "network.client.ip" #define DD_PREFIX_TAG_REQUEST_HEADER "http.request.headers." +#define DD_TAG_HTTP_REQH_CONTENT_TYPE "http.request.headers.content-type" +#define DD_TAG_HTTP_REQH_CONTENT_LENGTH "http.request.headers.content-length" #define DD_TAG_HTTP_RH_CONTENT_LENGTH "http.response.headers.content-length" #define DD_TAG_HTTP_RH_CONTENT_TYPE "http.response.headers.content-type" #define DD_TAG_HTTP_RH_CONTENT_ENCODING "http.response.headers.content-encoding" @@ -78,6 +80,8 @@ static zend_string *_dd_tag_http_status_code_zstr; static zend_string *_dd_tag_http_url_zstr; static zend_string *_dd_tag_network_client_ip_zstr; static zend_string *_dd_tag_http_client_ip_zstr; +static zend_string *_dd_tag_content_type; +static zend_string *_dd_tag_content_length; static zend_string *_dd_tag_rh_content_length; // response static zend_string *_dd_tag_rh_content_type; // response static zend_string *_dd_tag_rh_content_encoding; // response @@ -152,6 +156,10 @@ void dd_tags_startup() zend_string_init_interned(LSTRARG(DD_TAG_NETWORK_CLIENT_IP), 1); _dd_tag_http_client_ip_zstr = zend_string_init_interned(LSTRARG(DD_TAG_HTTP_CLIENT_IP), 1); + _dd_tag_content_type = + zend_string_init_interned(LSTRARG(DD_TAG_HTTP_REQH_CONTENT_TYPE), 1); + _dd_tag_content_length = + zend_string_init_interned(LSTRARG(DD_TAG_HTTP_REQH_CONTENT_LENGTH), 1); _dd_tag_rh_content_length = zend_string_init_interned(LSTRARG(DD_TAG_HTTP_RH_CONTENT_LENGTH), 1); @@ -657,6 +665,19 @@ static void _dd_http_client_ip(zend_array *meta_ht) } } +static void _try_add_tag(zend_array *meta_ht, zend_string *tag_name, zval *val) +{ + + Z_TRY_ADDREF_P(val); + bool added = zend_hash_add(meta_ht, tag_name, val) != NULL; + if (added) { + mlog(dd_log_debug, "Adding request header tag '%s' -> '%s", + ZSTR_VAL(tag_name), ZSTR_VAL(Z_STR_P(val))); + } else { + zval_delref_p(val); + } +} + static void _dd_request_headers( // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) zend_array *meta_ht, const zend_array *nonnull _server, @@ -675,6 +696,12 @@ static void _dd_request_headers( continue; } + if (zend_string_equals_literal(key, "CONTENT_TYPE")) { + _try_add_tag(meta_ht, _dd_tag_content_type, val); + } else if (zend_string_equals_literal(key, "CONTENT_LENGTH")) { + _try_add_tag(meta_ht, _dd_tag_content_length, val); + } + if (ZSTR_LEN(key) <= LSTRLEN("HTTP_") || memcmp(ZSTR_VAL(key), LSTRARG("HTTP_")) != 0) { continue; @@ -698,14 +725,7 @@ static void _dd_request_headers( continue; } - Z_TRY_ADDREF_P(val); - bool added = zend_hash_add(meta_ht, tag_name, val) != NULL; - if (added) { - mlog(dd_log_debug, "Adding request header tag '%s' -> '%s", - ZSTR_VAL(tag_name), ZSTR_VAL(Z_STR_P(val))); - } else { - zval_delref_p(val); - } + _try_add_tag(meta_ht, tag_name, val); zend_string_release(tag_name); } ZEND_HASH_FOREACH_END(); diff --git a/appsec/tests/extension/rinit_body_json.phpt b/appsec/tests/extension/rinit_body_json.phpt new file mode 100644 index 00000000000..d9c8cc35cad --- /dev/null +++ b/appsec/tests/extension/rinit_body_json.phpt @@ -0,0 +1,34 @@ +--TEST-- +request_init data on JSON data +--INI-- +datadog.appsec.testing_raw_body=1 +datadog.appsec.enabled=1 +--POST_RAW-- +{"foo":"bar"} +--ENV-- +CONTENT_TYPE=application/json +--FILE-- +get_commands(); + +var_dump($c[1][1][0]['server.request.body']); +var_dump($c[1][1][0]['server.request.body.raw']); + +?> +--EXPECT-- +bool(true) +array(1) { + ["foo"]=> + string(3) "bar" +} +string(13) "{"foo":"bar"}" diff --git a/appsec/tests/extension/rinit_body_xml.phpt b/appsec/tests/extension/rinit_body_xml.phpt index da0d9d666f9..b7f37e8566a 100644 --- a/appsec/tests/extension/rinit_body_xml.phpt +++ b/appsec/tests/extension/rinit_body_xml.phpt @@ -21,9 +21,15 @@ var_dump(rinit()); $c = $helper->get_commands(); +var_dump($c[1][1][0]['server.request.body']); var_dump($c[1][1][0]['server.request.body.raw']); ?> --EXPECT-- bool(true) +array(1) { + ["foo"]=> + array(0) { + } +} string(6) "" diff --git a/appsec/tests/extension/user_req_xml_req.phpt b/appsec/tests/extension/user_req_xml_req.phpt new file mode 100644 index 00000000000..74d6dfe08e9 --- /dev/null +++ b/appsec/tests/extension/user_req_xml_req.phpt @@ -0,0 +1,82 @@ +--TEST-- +User requests: request body is parsed (XML) +--INI-- +extension=ddtrace.so +datadog.appsec.enabled=true +datadog.appsec.cli_start_on_rinit=false +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- + + +test
baz +
+XML; + +$res = notify_start($span, array( + '_SERVER' => [ + 'REMOTE_ADDR' => '1.2.3.4', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/foo', + 'SERVER_NAME' => 'example.com', + 'SERVER_PORT' => 80, + 'HTTP_HOST' => 'example2.com', + 'HTTP_CLIENT_IP' => '2.3.4.5', + 'CONTENT_TYPE' => 'text/xml', + ], +), $xml); +echo "Result of notify_start:\n"; +var_dump($res); + +close_span(100.0); + +$c = $helper->get_commands(); +print_r($c[1][1][0]['server.request.body']); +--EXPECT-- +Result of notify_start: +NULL +Array +( + [foo] => Array + ( + [content] => Array + ( + [0] => +test + [1] => Array + ( + [br] => Array + ( + ) + + ) + + [2] => baz + + ) + + [attributes] => Array + ( + [attr] => bar + ) + + ) + +) diff --git a/appsec/tests/extension/user_req_xml_req_wrong_ct.phpt b/appsec/tests/extension/user_req_xml_req_wrong_ct.phpt new file mode 100644 index 00000000000..492c3ba82bd --- /dev/null +++ b/appsec/tests/extension/user_req_xml_req_wrong_ct.phpt @@ -0,0 +1,57 @@ +--TEST-- +User requests: request body is parsed (XML) +--INI-- +extension=ddtrace.so +datadog.appsec.enabled=true +datadog.appsec.cli_start_on_rinit=false +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- + + +test
baz +
+XML; + +$res = notify_start($span, array( + '_SERVER' => [ + 'REMOTE_ADDR' => '1.2.3.4', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/foo', + 'SERVER_NAME' => 'example.com', + 'SERVER_PORT' => 80, + 'HTTP_HOST' => 'example2.com', + 'HTTP_CLIENT_IP' => '2.3.4.5', + 'HTTP_CONTENT_TYPE' => 'text/NOT_XML', + ], +), $xml); +echo "Result of notify_start:\n"; +var_dump($res); + +close_span(100.0); + +$c = $helper->get_commands(); +print_r($c[1][1][0]['server.request.body']); +--EXPECT-- +Result of notify_start: +NULL +Array +( +) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy index eddc166801f..3d757fe5a8b 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy @@ -330,6 +330,22 @@ trait CommonTests { assertThat appsecJson, matchesJson(expJson, false, true) } + + @Test + void 'POST request sets content type and length'() { + def json = '{"message":["Hello world!"]}' + HttpRequest req = container.buildReq('/hello.php') + .header('Content-type', 'application/json') + .POST(HttpRequest.BodyPublishers.ofString(json)).build() + def trace = container.traceFromRequest(req, ofString()) { HttpResponse resp -> + assert resp.body() == 'Hello world!' + } + + Span span = trace.first() + assert span.meta['http.request.headers.content-type'] == 'application/json' + assert span.meta['http.request.headers.content-length'] == '28' + } + @Test void 'module does not have STATIC_TLS flag'() { Container.ExecResult res = container.execInContainer( diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy index f1b291598ad..e727aad3cd4 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy @@ -214,7 +214,7 @@ class RoadRunnerTests { @Test - void 'blocking against a json body'() { + void 'blocking against a json response body'() { HttpRequest req = CONTAINER.buildReq('/json?block=1').GET().build() def trace = CONTAINER.traceFromRequest(req, ofString()) { HttpResponse resp -> assert resp.body().containsIgnoreCase("You've been blocked") @@ -309,4 +309,109 @@ class RoadRunnerTests { }''' assertThat appsecJson, matchesJson(expJson, false, true) } + + + @Test + void 'match against json request body'() { + def json = '{"message":["Hello world!",42,true,"poison"]}' + HttpRequest req = CONTAINER.buildReq('/') + .header('Content-type', 'application/json') + .POST(HttpRequest.BodyPublishers.ofString(json)).build() + def trace = CONTAINER.traceFromRequest(req, ofString()) { HttpResponse resp -> + assert resp.body() == 'Hello world!' + } + + Span span = trace.first() + assert span.meta['http.request.headers.content-type'] == 'application/json' + assert span.meta['http.request.headers.content-length'] == '45' + + def appsecJson = span.meta."_dd.appsec.json" + def expJson = '''{ + "triggers" : [ + { + "rule" : { + "id" : "poison-in-json", + "name" : "poison-in-json", + "tags" : { + "category" : "attack_attempt", + "type" : "security_scanner" + } + }, + "rule_matches" : [ + { + "operator" : "match_regex", + "operator_value" : "(?i)poison", + "parameters" : [ + { + "address" : "server.request.body", + "highlight" : [ + "poison" + ], + "key_path" : [ + "message", + "3" + ], + "value" : "poison" + } + ] + } + ] + } + ] + }''' + assertThat appsecJson, matchesJson(expJson, false, true) + } + + @Test + void 'match against xml request body'() { + def xml = ''' + + Jeanpoison''' + HttpRequest req = CONTAINER.buildReq('/') + .header('Content-type', 'application/xml') + .POST(HttpRequest.BodyPublishers.ofString(xml)).build() + def trace = CONTAINER.traceFromRequest(req, ofString()) { HttpResponse resp -> + assert resp.body() == 'Hello world!' + } + + Span span = trace.first() + + def appsecJson = span.meta."_dd.appsec.json" + def expJson = '''{ + "triggers" : [ + { + "rule" : { + "id" : "poison-in-xml", + "name" : "poison-in-xml", + "tags" : { + "category" : "attack_attempt", + "type" : "security_scanner" + } + }, + "rule_matches" : [ + { + "operator" : "match_regex", + "operator_value" : "(?i).*poison.*", + "parameters" : [ + { + "address" : "server.request.body", + "highlight" : [ + "poison" + ], + "key_path" : [ + "note", + "content", + "1" + ], + "value" : "poison" + } + ] + } + ] + } + ] + }''' + + assertThat appsecJson, matchesJson(expJson, false, true) + } } diff --git a/appsec/tests/integration/src/test/waf/recommended.json b/appsec/tests/integration/src/test/waf/recommended.json index be954478c5f..4a4fc852512 100644 --- a/appsec/tests/integration/src/test/waf/recommended.json +++ b/appsec/tests/integration/src/test/waf/recommended.json @@ -6698,6 +6698,12 @@ { "parameters": { "inputs": [ + { + "address": "server.request.body", + "key_path": [ + "message" + ] + }, { "address": "server.response.body", "key_path": [ @@ -6751,6 +6757,13 @@ { "parameters": { "inputs": [ + { + "address": "server.request.body", + "key_path": [ + "note", + "content" + ] + }, { "address": "server.response.body", "key_path": [ diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index 8ae9f9a014f..a985400aeca 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -630,9 +630,10 @@ function has_listeners(): bool {} * * @param \DDTrace\Span $span the span associated with this user request. * @param array $data an array with keys named '_GET', '_POST', '_SERVER', '_FILES', '_COOKIE' + * @param string|resource|null $body the body of the request (a string or a seekable resource) * @return array|null an array with the keys 'status', 'headers' and 'body', or null */ - function notify_start(\DDTrace\RootSpanData $span, array $data): ?array {} + function notify_start(\DDTrace\RootSpanData $span, array $data, ?mixed $body = null): ?array {} /** * Notifies the user request listeners of the imminence of a commit, and allows for the replacement of the response. diff --git a/ext/ddtrace_arginfo.h b/ext/ddtrace_arginfo.h index 78bbd8319e4..d7a28681322 100644 --- a/ext/ddtrace_arginfo.h +++ b/ext/ddtrace_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: efb73e985a3faa7e239d87b716a87dd5ce9b5f9c */ + * Stub hash: 899dcc72fc2a852d8f1e8aad895c4b628cd17eb8 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_trace_method, 0, 3, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, className, IS_STRING, 0) @@ -145,6 +145,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_UserRequest_notify_start, 0, 2, IS_ARRAY, 1) ZEND_ARG_OBJ_INFO(0, span, DDTrace\\RootSpanData, 0) ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, body, IS_MIXED, 1, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_UserRequest_notify_commit, 0, 3, IS_ARRAY, 1) diff --git a/ext/serializer.c b/ext/serializer.c index eb863364b1b..d6f887cd052 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -1790,10 +1790,9 @@ void ddtrace_error_cb(DDTRACE_ERROR_CB_PARAMETERS) { ddtrace_prev_error_cb(DDTRACE_ERROR_CB_PARAM_PASSTHRU); } - -static zend_array *dd_ser_start_user_req(ddtrace_user_req_listeners *self, zend_object *span, zend_array *variables) -{ +static zend_array *dd_ser_start_user_req(ddtrace_user_req_listeners *self, zend_object *span, zend_array *variables, zval *entity) { UNUSED(self); + UNUSED(entity); struct superglob_equiv data = {0}; zval *_server_zv = zend_hash_str_find(variables, ZEND_STRL("_SERVER")); diff --git a/ext/user_request.c b/ext/user_request.c index cd67bb0701a..d5fe47a171d 100644 --- a/ext/user_request.c +++ b/ext/user_request.c @@ -52,10 +52,13 @@ PHP_FUNCTION(DDTrace_UserRequest_notify_start) { zend_object *span; zend_array *array; + zval *rbe_zv = NULL; - ZEND_PARSE_PARAMETERS_START(2, 2) + ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_OBJ_OF_CLASS_EX(span, ddtrace_ce_root_span_data, 0, 1) Z_PARAM_ARRAY_HT(array) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(rbe_zv) ZEND_PARSE_PARAMETERS_END(); ddtrace_span_data *span_data = OBJ_SPANDATA(span); @@ -71,7 +74,7 @@ PHP_FUNCTION(DDTrace_UserRequest_notify_start) zend_array *replacement_resp = NULL; for (size_t i = 0; i < reg_listeners.size; i++) { ddtrace_user_req_listeners *listener = reg_listeners.listeners[i]; - zend_array *repl = listener->start_user_req(listener, span, array); + zend_array *repl = listener->start_user_req(listener, span, array, rbe_zv); if (repl != NULL && replacement_resp == NULL) { replacement_resp = repl; } else if (repl != NULL) { diff --git a/ext/user_request.h b/ext/user_request.h index f5862902ea9..f36a4664148 100644 --- a/ext/user_request.h +++ b/ext/user_request.h @@ -6,7 +6,8 @@ typedef struct _ddtrace_user_req_listeners ddtrace_user_req_listeners; struct _ddtrace_user_req_listeners { int priority; - zend_array *(*start_user_req)(ddtrace_user_req_listeners *self, zend_object *span, zend_array *variables); + // entity is nullable + zend_array *(*start_user_req)(ddtrace_user_req_listeners *self, zend_object *span, zend_array *variables, zval *entity); // headers is an array string => array(string). The header names are not normalized. entity is nullable. zend_array *(*response_committed)(ddtrace_user_req_listeners *self, zend_object *span, int status, zend_array *headers, zval *entity); diff --git a/src/Integrations/Integrations/Roadrunner/RoadrunnerIntegration.php b/src/Integrations/Integrations/Roadrunner/RoadrunnerIntegration.php index 7c31cd89623..849a7bda97f 100644 --- a/src/Integrations/Integrations/Roadrunner/RoadrunnerIntegration.php +++ b/src/Integrations/Integrations/Roadrunner/RoadrunnerIntegration.php @@ -63,7 +63,11 @@ public static function build_req_spec(\Spiral\RoadRunner\Http\Request $req) { foreach ($req->headers as $name => $values) { $collapsedValue = implode(', ', $values); $name = preg_replace("/[^A-Z\d]/", "_", strtoupper($name)); - $server["HTTP_$name"] = $collapsedValue; + // these two have special treatment. See RFC 3875 + if ($name != 'CONTENT_TYPE' && $name != 'CONTENT_LENGTH') { + $name = "HTTP_$name"; + } + $server[$name] = $collapsedValue; } $ret['_SERVER'] = $server; @@ -188,7 +192,7 @@ function (HookData $hook) use (&$activeSpan, &$suppressResponse, $integration, $ return $headers[$headername] ?? null; }); - $res = notify_start($activeSpan, RoadrunnerIntegration::build_req_spec($retval)); + $res = notify_start($activeSpan, RoadrunnerIntegration::build_req_spec($retval), $retval->body); if ($res) { // block on start RoadrunnerIntegration::ensure_headers_map_fmt($res['headers']);