Skip to content

Commit 8b890b8

Browse files
authored
Send raw response bodies to the waf (#4055)
1 parent af463f7 commit 8b890b8

23 files changed

Lines changed: 405 additions & 43 deletions

appsec/helper-rust/src/client/protocol.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ use tokio_util::codec::{Decoder, Encoder};
1313
use crate::client::log::{fmt_bin, trace};
1414

1515
pub const VERSION_FOR_PROTO: &str = env!("DDAPPSEC_VERSION");
16-
const MAX_MESSAGE_SIZE: u32 = 4 * 1024 * 1024;
16+
// Incoming messages (PHP extension → helper) can be large when raw response
17+
// body is enabled; both parsed and raw body can each be up to 4 MiB.
18+
const MAX_INCOMING_MSG_SIZE: u32 = 10 * 1024 * 1024;
19+
// Outgoing responses (helper → PHP extension) are small by design; keep this
20+
// at 4 MiB to stay within the PHP extension's receive buffer.
21+
const MAX_OUTGOING_MSG_SIZE: u32 = 4 * 1024 * 1024;
1722

1823
#[derive(Debug)]
1924
pub enum Command {
@@ -367,11 +372,11 @@ impl Decoder for CommandCodec {
367372
));
368373
}
369374

370-
if header.size > MAX_MESSAGE_SIZE {
375+
if header.size > MAX_INCOMING_MSG_SIZE {
371376
return Err(io::Error::new(
372377
io::ErrorKind::InvalidData,
373378
format!(
374-
"Message is too large: {} bytes (supported up to 4 MB)",
379+
"Message is too large: {} bytes (supported up to 10 MiB)",
375380
header.size
376381
),
377382
));
@@ -475,12 +480,12 @@ impl Encoder<CommandResponse<'_>> for CommandCodec {
475480
}
476481

477482
let size = dst.len() - start - header_len;
478-
if size > MAX_MESSAGE_SIZE as usize {
483+
if size > MAX_OUTGOING_MSG_SIZE as usize {
479484
dst.truncate(start);
480485
return Err(io::Error::new(
481486
io::ErrorKind::InvalidData,
482487
format!(
483-
"Message is too large: {} bytes (supported up to 4 MB)",
488+
"Message is too large: {} bytes (supported up to 4 MiB)",
484489
size
485490
),
486491
));

appsec/src/extension/commands/request_shutdown.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "request_shutdown.h"
88
#include "../commands_helpers.h"
9+
#include "../configuration.h"
910
#include "../ddappsec.h"
1011
#include "../ddtrace.h"
1112
#include "../entity_body.h"
@@ -62,7 +63,12 @@ static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull ctx)
6263
}
6364
}
6465

65-
mpack_start_map(w, 2 + (Z_TYPE(resp_body) != IS_NULL ? 1 : 0));
66+
bool send_raw_body = get_global_DD_APPSEC_RAW_RESPONSE_BODY_ENABLED() &&
67+
req_info->entity != NULL && req_info->entity->len > 0;
68+
69+
uint32_t num_entries =
70+
2 + (Z_TYPE(resp_body) != IS_NULL ? 1 : 0) + (send_raw_body ? 1 : 0);
71+
mpack_start_map(w, num_entries);
6672

6773
// 1.1.
6874
{
@@ -94,6 +100,13 @@ static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull ctx)
94100
}
95101
}
96102

103+
// 1.4.?
104+
if (send_raw_body) {
105+
dd_mpack_write_lstr(w, "server.response.body.raw");
106+
mpack_write_bin(w, ZSTR_VAL(req_info->entity),
107+
(uint32_t)ZSTR_LEN(req_info->entity));
108+
}
109+
97110
mpack_finish_map(w);
98111

99112
// 2.

appsec/src/extension/configuration.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ extern bool runtime_config_first_init;
6262
CONFIG(STRING, DD_AGENT_HOST, "localhost") \
6363
CONFIG(INT, DD_TRACE_AGENT_PORT, "0") \
6464
CONFIG(INT, DD_APPSEC_MAX_BODY_BUFF_SIZE, "524288") \
65+
SYSCFG(BOOL, DD_APPSEC_RAW_RESPONSE_BODY_ENABLED, "false") \
6566
CONFIG(STRING, DD_TRACE_AGENT_URL, "") \
6667
CONFIG(BOOL, DD_TRACE_ENABLED, "true") \
6768
CALIAS(CUSTOM(STRING), DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE, "ident", \

appsec/src/extension/entity_body.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ static typeof(zend_write(NULL, 0)) _dd_save_output_zend_write(
9898
return orig_zend_write(str, str_length);
9999
}
100100

101+
// Maximum response body buffer when raw body sending is enabled. Two bodies
102+
// (parsed + raw) must fit within the helper protocol limit (10 MiB) with room
103+
// for headers and other fields.
104+
#define MAX_BODY_BUFF_RAW_ENABLED (4UL * 1024UL * 1024UL)
105+
101106
void dd_entity_body_rinit(void)
102107
{
103108
zend_long conf_size = get_DD_APPSEC_MAX_BODY_BUFF_SIZE();
@@ -108,7 +113,20 @@ void dd_entity_body_rinit(void)
108113
desired_bufsize = (size_t)conf_size;
109114
}
110115

116+
size_t uncapped_bufsize = desired_bufsize;
117+
if (get_global_DD_APPSEC_RAW_RESPONSE_BODY_ENABLED() &&
118+
desired_bufsize > MAX_BODY_BUFF_RAW_ENABLED) {
119+
desired_bufsize = MAX_BODY_BUFF_RAW_ENABLED;
120+
}
121+
111122
if (desired_bufsize != _buffer_size) {
123+
if (uncapped_bufsize != desired_bufsize) {
124+
mlog(dd_log_warning,
125+
"DD_APPSEC_MAX_BODY_BUFF_SIZE (%zu) exceeds maximum allowed "
126+
"when DD_APPSEC_RAW_RESPONSE_BODY_ENABLED is set (%zu); "
127+
"capping",
128+
uncapped_bufsize, (size_t)MAX_BODY_BUFF_RAW_ENABLED);
129+
}
112130
if (_buffer != NULL) {
113131
zend_string_release(_buffer);
114132
}

appsec/src/helper/network/broker.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class base_broker {
3030

3131
class broker : public base_broker {
3232
public:
33-
// message body size limit (700 KB)
34-
static constexpr std::size_t max_msg_body_size = 750UL * 1024UL;
33+
// message body size limit (10 MiB)
34+
static constexpr std::size_t max_msg_body_size = 10UL * 1024UL * 1024UL;
3535

3636
explicit broker(std::unique_ptr<base_socket> &&socket)
3737
: socket_(std::move(socket))

appsec/src/helper/network/msgpack_helpers.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ dds::parameter msgpack_to_param(const msgpack::object &o, unsigned depth = 0)
4343
}
4444
case msgpack::type::STR:
4545
return dds::parameter::string(o.as<std::string_view>());
46+
case msgpack::type::BIN:
47+
return dds::parameter::string(
48+
std::string_view{o.via.bin.ptr, o.via.bin.size});
4649
case msgpack::type::BOOLEAN:
4750
return dds::parameter::as_boolean(o.as<bool>());
4851
case msgpack::type::FLOAT64:
6 Bytes
Binary file not shown.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
request_shutdown — server.response.body.raw not sent when feature is disabled (default)
3+
--INI--
4+
expose_php=0
5+
datadog.appsec.enabled=1
6+
--GET--
7+
a=b
8+
--FILE--
9+
<?php
10+
use function datadog\appsec\testing\{rinit,rshutdown};
11+
12+
include __DIR__ . '/inc/mock_helper.php';
13+
14+
$helper = Helper::createInitedRun([
15+
response_list(response_request_init([[['ok', []]]])),
16+
response_list(response_request_shutdown([[['ok', []]], new ArrayObject(), new ArrayObject()]))
17+
]);
18+
19+
header('content-type: text/plain');
20+
http_response_code(200);
21+
var_dump(rinit());
22+
echo "plain text body\n";
23+
$helper->get_commands(); // ignore
24+
25+
var_dump(rshutdown());
26+
$c = $helper->get_commands();
27+
var_dump(array_key_exists('server.response.body.raw', $c[0][1][0]));
28+
?>
29+
--EXPECT--
30+
bool(true)
31+
plain text body
32+
bool(true)
33+
bool(false)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
request_shutdown — server.response.body.raw sent alongside parsed server.response.body for JSON
3+
--INI--
4+
expose_php=0
5+
datadog.appsec.enabled=1
6+
datadog.appsec.raw_response_body_enabled=1
7+
--GET--
8+
a=b
9+
--FILE--
10+
<?php
11+
use function datadog\appsec\testing\{rinit,rshutdown};
12+
13+
include __DIR__ . '/inc/mock_helper.php';
14+
15+
$helper = Helper::createInitedRun([
16+
response_list(response_request_init([[['ok', []]]])),
17+
response_list(response_request_shutdown([[['ok', []]], new ArrayObject(), new ArrayObject()]))
18+
]);
19+
20+
header('content-type: application/json');
21+
http_response_code(200);
22+
var_dump(rinit());
23+
echo '{"key":"value"}', "\n";
24+
$helper->get_commands(); // ignore
25+
26+
var_dump(rshutdown());
27+
$c = $helper->get_commands();
28+
$data = $c[0][1][0];
29+
30+
// both structured and raw body are present
31+
var_dump(isset($data['server.response.body']));
32+
var_dump(isset($data['server.response.body.raw']));
33+
print_r($data['server.response.body']);
34+
echo $data['server.response.body.raw'];
35+
?>
36+
--EXPECT--
37+
bool(true)
38+
{"key":"value"}
39+
bool(true)
40+
bool(true)
41+
bool(true)
42+
Array
43+
(
44+
[key] => value
45+
)
46+
{"key":"value"}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
request_shutdown — server.response.body.raw sent for non-JSON/XML content types
3+
--INI--
4+
expose_php=0
5+
datadog.appsec.enabled=1
6+
datadog.appsec.raw_response_body_enabled=1
7+
--GET--
8+
a=b
9+
--FILE--
10+
<?php
11+
use function datadog\appsec\testing\{rinit,rshutdown};
12+
13+
include __DIR__ . '/inc/mock_helper.php';
14+
15+
$helper = Helper::createInitedRun([
16+
response_list(response_request_init([[['ok', []]]])),
17+
response_list(response_request_shutdown([[['ok', []]], new ArrayObject(), new ArrayObject()]))
18+
]);
19+
20+
header('content-type: text/plain');
21+
http_response_code(200);
22+
var_dump(rinit());
23+
echo "plain text body\n";
24+
$helper->get_commands(); // ignore
25+
26+
var_dump(rshutdown());
27+
$c = $helper->get_commands();
28+
$data = $c[0][1][0];
29+
30+
// server.response.body is absent for non-JSON/XML
31+
var_dump(isset($data['server.response.body']));
32+
// server.response.body.raw is present with the raw text
33+
var_dump(isset($data['server.response.body.raw']));
34+
echo $data['server.response.body.raw'];
35+
?>
36+
--EXPECT--
37+
bool(true)
38+
plain text body
39+
bool(true)
40+
bool(false)
41+
bool(true)
42+
plain text body

0 commit comments

Comments
 (0)