Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd5e0aa
Allow partial processing of XML and JSON request body
hnakamur Dec 16, 2025
eb58618
Use spaces for indentations in regression/config_body_limits.json
hnakamur Dec 17, 2025
8699723
Remove trailing spaces in regression test cases
hnakamur Dec 17, 2025
6ba8eac
Add regression test cases for SecRequestBodyLimitAction ProcessPartial
hnakamur Dec 17, 2025
b77a1a7
Use unique_ptr for XML and JSON request body processors
hnakamur Dec 17, 2025
7e0f58f
Do partial processing if action is ProcessPartial and request body si…
hnakamur Dec 18, 2025
1a8a04c
Stop using unique_ptr for XML and JSON request body processor
hnakamur Dec 18, 2025
c981a16
Skip tests using XML request processor when libxml2 is unavailable
hnakamur Dec 29, 2025
2a7c385
Add debug log for allowing partial processing of JSON and XML
hnakamur Dec 29, 2025
400e871
Stop adding LF to the end of each body line in regression tests
hnakamur Dec 28, 2025
6382a3c
Append LF to request and response body lines in regression test cases
hnakamur Dec 29, 2025
0b9cc9d
Fix indent in src/request_body_processor/multipart.cc
hnakamur Dec 29, 2025
2268a99
Allow partial multipart for body larger than SecRequestBodyLimit
hnakamur Dec 29, 2025
1647cc3
Add tests for parsing multipart body with SecRequestBodyLimitAction P…
hnakamur Dec 29, 2025
22b3429
Accept partial epilogue in body larger than limit for ProcessPartial
hnakamur Dec 31, 2025
9cf02ca
Add regression test option to always show log
hnakamur Dec 28, 2025
36c5c39
Add regression test option to show request body size
hnakamur Dec 29, 2025
4925d4c
Fix warnings reported by cppcheck
hnakamur Dec 31, 2025
8acf823
Align ProcessPartial tests with v2
hnakamur Jan 1, 2026
33db296
Modify url-encoded reqbody tests for ProcesPartial
hnakamur Jan 2, 2026
474ec6e
Support partial processing of url-encoded reqbody
hnakamur Jan 2, 2026
ea70150
Add tests for MULTIPART_PART_HEADERS with ProcessPartial
hnakamur Jan 3, 2026
eb9e413
Reject invalid final boundary for multipart with ProcessPartial
hnakamur Jan 3, 2026
086c95c
Modify tests for multipart with ProcessPartial
hnakamur Jan 3, 2026
ec94ffc
Modify multipart_complete for incomplete final boundary
hnakamur Jan 3, 2026
60d149e
Process an incomplete boundary as a non-final boundary
hnakamur Jan 3, 2026
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
7 changes: 6 additions & 1 deletion headers/modsecurity/transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ class Transaction : public TransactionAnchoredVariables, public TransactionSecMa
bool addArgument(const std::string& orig, const std::string& key,
const std::string& value, size_t offset);
bool extractArguments(const std::string &orig, const std::string& buf,
size_t offset);
size_t offset, bool partial_processing_enabled = false);

const char *getResponseBody() const;
size_t getResponseBodyLength();
Expand Down Expand Up @@ -645,6 +645,11 @@ class Transaction : public TransactionAnchoredVariables, public TransactionSecMa
* the web server (connector) log.
*/
void *m_logCbData;

/**
* Whether the request body was bigger than RequestBodyLimit.
*/
bool m_requestBodyLimitExceeded;
};


Expand Down
8 changes: 4 additions & 4 deletions src/request_body_processor/json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ namespace RequestBodyProcessor {
static const double json_depth_limit_default = 10000.0;
static const char* json_depth_limit_exceeded_msg = ". Parsing depth limit exceeded";

JSON::JSON(Transaction *transaction) : m_transaction(transaction),
JSON::JSON(Transaction *transaction)
: m_transaction(transaction),
m_handle(NULL),
m_current_key(""),
m_max_depth(json_depth_limit_default),
Expand Down Expand Up @@ -68,8 +69,6 @@ JSON::JSON(Transaction *transaction) : m_transaction(transaction),
* TODO: make UTF8 validation optional, as it depends on Content-Encoding
*/
m_handle = yajl_alloc(&callbacks, NULL, this);

yajl_config(m_handle, yajl_allow_partial_values, 0);
}


Expand All @@ -83,7 +82,8 @@ JSON::~JSON() {
}


bool JSON::init() {
bool JSON::init(unsigned int allow_partial_values) {
yajl_config(m_handle, yajl_allow_partial_values, allow_partial_values);
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/request_body_processor/json.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class JSON {
explicit JSON(Transaction *transaction);
~JSON();

static bool init();
bool init(unsigned int allow_partial_values = 0);
bool processChunk(const char *buf, unsigned int size, std::string *err);
bool complete(std::string *err);

Expand Down
134 changes: 95 additions & 39 deletions src/request_body_processor/multipart.cc
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Multipart::Multipart(const std::string &header, Transaction *transaction)
m_reserve{0},
m_seen_data(0),
m_is_complete(0),
m_allow_partial(false),
m_flag_error(0),
m_flag_data_before(0),
m_flag_data_after(0),
Expand Down Expand Up @@ -1151,53 +1152,108 @@ int Multipart::multipart_complete(std::string *error) {
* processed yet) in the buffer.
*/
if (m_buf_contains_line) {
if (((unsigned int)(MULTIPART_BUF_SIZE - m_bufleft)
== (4 + m_boundary.size()))
/*
* Note that the buffer may end with the final boundary followed by only CR,
* coming from the [CRLF epilogue], when allow_process_partial == 1 (which is
* set when SecRequestBodyLimitAction is ProcessPartial and the request body
* length exceeds SecRequestBodyLimit).
*
* The following definitions are copied from RFC 2046:
*
* dash-boundary := "--" boundary
*
* delimiter := CRLF dash-boundary
*
* close-delimiter := delimiter "--"
*
* multipart-body := [preamble CRLF]
* dash-boundary transport-padding CRLF
* body-part *encapsulation
* close-delimiter transport-padding
* [CRLF epilogue]
*/
unsigned int buf_data_len = (unsigned int)(MULTIPART_BUF_SIZE - m_bufleft);
if ((buf_data_len >= 2 + m_boundary.size())
&& (*(m_buf) == '-')
&& (*(m_buf + 1) == '-')
&& (strncmp(m_buf + 2, m_boundary.c_str(),
m_boundary.size()) == 0)
&& (*(m_buf + 2 + m_boundary.size()) == '-')
&& (*(m_buf + 2 + m_boundary.size() + 1) == '-')) {
// these next two checks may result in repeating work from earlier in this fn
// ignore the duplication for now to minimize refactoring
if ((m_crlf_state_buf_end == 2) && (m_flag_lf_line != 1)) {
m_flag_lf_line = 1;
m_transaction->m_variableMultipartLFLine.set(std::to_string(m_flag_lf_line),
m_transaction->m_variableOffset);
m_transaction->m_variableMultipartCrlfLFLines.set(std::to_string(m_flag_crlf_line && m_flag_lf_line),
m_transaction->m_variableOffset);
if (m_flag_crlf_line && m_flag_lf_line) {
ms_dbg_a(m_transaction, 4, "Multipart: Warning: mixed line endings used (CRLF/LF).");
} else if (m_flag_lf_line) {
ms_dbg_a(m_transaction, 4, "Multipart: Warning: incorrect line endings used (LF).");
m_boundary.size()) == 0)) {
if ((buf_data_len >= 2 + m_boundary.size() + 2)
&& (*(m_buf + 2 + m_boundary.size()) == '-')
&& (*(m_buf + 2 + m_boundary.size() + 1) == '-')) {
/* If body fits in limit and ends with final boundary plus just CR, reject it. */
if ( (m_allow_partial == 0)
&& (buf_data_len == 2 + m_boundary.size() + 2 + 1)
&& (*(m_buf + 2 + m_boundary.size() + 2) == '\r') ) {
ms_dbg_a(m_transaction, 1,
"Multipart: Invalid epilogue after final boundary.");
error->assign("Multipart: Invalid epilogue after final boundary.");
return false;
}
// these next two checks may result in repeating work from earlier in this fn
// ignore the duplication for now to minimize refactoring
if ((m_crlf_state_buf_end == 2) && (m_flag_lf_line != 1)) {
m_flag_lf_line = 1;
m_transaction->m_variableMultipartLFLine.set(std::to_string(m_flag_lf_line),
m_transaction->m_variableOffset);
m_transaction->m_variableMultipartCrlfLFLines.set(std::to_string(m_flag_crlf_line && m_flag_lf_line),
m_transaction->m_variableOffset);
if (m_flag_crlf_line && m_flag_lf_line) {
ms_dbg_a(m_transaction, 4, "Multipart: Warning: mixed line endings used (CRLF/LF).");
} else if (m_flag_lf_line) {
ms_dbg_a(m_transaction, 4, "Multipart: Warning: incorrect line endings used (LF).");
}
m_transaction->m_variableMultipartStrictError.set(
std::to_string(m_flag_lf_line) , m_transaction->m_variableOffset);
}
if ((m_mpp_substate_part_data_read == 0) && (m_flag_invalid_part != 1)) {
// it looks like the final boundary, but it's where part data should begin
m_flag_invalid_part = 1;
ms_dbg_a(m_transaction, 3, "Multipart: Invalid part (data contains final boundary)");
m_transaction->m_variableMultipartStrictError.set(
std::to_string(m_flag_invalid_part) , m_transaction->m_variableOffset);
m_transaction->m_variableMultipartInvalidPart.set(std::to_string(m_flag_invalid_part),
m_transaction->m_variableOffset);
ms_dbg_a(m_transaction, 4, "Multipart: Warning: invalid part parsing.");
}
m_transaction->m_variableMultipartStrictError.set(
std::to_string(m_flag_lf_line) , m_transaction->m_variableOffset);
}
if ((m_mpp_substate_part_data_read == 0) && (m_flag_invalid_part != 1)) {
// it looks like the final boundary, but it's where part data should begin
m_flag_invalid_part = 1;
ms_dbg_a(m_transaction, 3, "Multipart: Invalid part (data contains final boundary)");
m_transaction->m_variableMultipartStrictError.set(
std::to_string(m_flag_invalid_part) , m_transaction->m_variableOffset);
m_transaction->m_variableMultipartInvalidPart.set(std::to_string(m_flag_invalid_part),
m_transaction->m_variableOffset);
ms_dbg_a(m_transaction, 4, "Multipart: Warning: invalid part parsing.");
}

/* Looks like the final boundary - process it. */
if (process_boundary(1 /* final */) < 0) {
m_flag_error = 1;
return -1;
}
/* Looks like the final boundary - process it. */
if (process_boundary(1 /* final */) < 0) {
m_flag_error = 1;
return -1;
}

/* The payload is complete after all. */
m_is_complete = 1;
/* The payload is complete after all. */
m_is_complete = 1;
} else if (m_allow_partial) {
if (buf_data_len >= 2 + m_boundary.size() + 1) {
if (*(m_buf + 2 + m_boundary.size()) == '-') {
if ((buf_data_len >= 2 + m_boundary.size() + 2)
&& (*(m_buf + 2 + m_boundary.size() + 1) != '-')) {
ms_dbg_a(m_transaction, 1,
"Multipart: Invalid final boundary.");
error->assign("Multipart: Invalid final boundary.");
return false;
}
} else if ((*(m_buf + 2 + m_boundary.size()) != '\r')
|| ((buf_data_len >= 2 + m_boundary.size() + 2)
&& (*(m_buf + 2 + m_boundary.size() + 1) != '\n'))) {
ms_dbg_a(m_transaction, 1,
"Multipart: Invalid boundary.");
error->assign("Multipart: Invalid boundary.");
return false;
}
}
/* process it as a non-final boundary to avoid building a new part. */
if (process_boundary(0) < 0) {
m_flag_error = 1;
return -1;
}
}
}
}

if (m_is_complete == 0) {
if (m_is_complete == 0 && !m_allow_partial) {
ms_dbg_a(m_transaction, 1,
"Multipart: Final boundary missing.");
error->assign("Multipart: Final boundary missing.");
Expand Down Expand Up @@ -1563,7 +1619,7 @@ bool Multipart::process(const std::string& data, std::string *error,
m_boundary.size()) == 0)) {
if (m_crlf_state_buf_end == 2) {
m_flag_lf_line = 1;
}
}
if ((m_mpp_substate_part_data_read == 0) && (m_boundary_count > 0)) {
/* string matches our boundary, but it's where part data should begin */
m_flag_invalid_part = 1;
Expand Down
1 change: 1 addition & 0 deletions src/request_body_processor/multipart.h
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ class Multipart {

int m_seen_data;
int m_is_complete;
bool m_allow_partial;

int m_flag_error;
int m_flag_data_before;
Expand Down
11 changes: 6 additions & 5 deletions src/request_body_processor/xml.cc
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ extern "C" {
}

XML::XML(Transaction *transaction)
: m_transaction(transaction) {
: m_transaction(transaction), m_require_well_formed(false) {
m_data.doc = NULL;
m_data.parsing_ctx = NULL;
m_data.sax_handler = NULL;
Expand All @@ -171,7 +171,8 @@ XML::~XML() {
}
}

bool XML::init() {
bool XML::init(bool require_well_formed) {
m_require_well_formed = require_well_formed;
//xmlParserInputBufferCreateFilenameFunc entity;
if (m_transaction->m_rules->m_secXMLExternalEntity
== RulesSetProperties::TrueConfigBoolean) {
Expand Down Expand Up @@ -280,7 +281,7 @@ bool XML::processChunk(const char *buf, unsigned int size,
!= RulesSetProperties::OnlyArgsConfigXMLParseXmlIntoArgs) {
xmlParseChunk(m_data.parsing_ctx, buf, size, 0);
m_data.xml_parser_state->parsing_ctx_arg = m_data.parsing_ctx_arg;
if (m_data.parsing_ctx->wellFormed != 1) {
if (m_require_well_formed && m_data.parsing_ctx->wellFormed != 1) {
error->assign("XML: Failed to parse document.");
ms_dbg_a(m_transaction, 4, "XML: Failed to parse document.");
return false;
Expand All @@ -296,7 +297,7 @@ bool XML::processChunk(const char *buf, unsigned int size,
== RulesSetProperties::TrueConfigXMLParseXmlIntoArgs)
) {
xmlParseChunk(m_data.parsing_ctx_arg, buf, size, 0);
if (m_data.parsing_ctx_arg->wellFormed != 1) {
if (m_require_well_formed && m_data.parsing_ctx_arg->wellFormed != 1) {
error->assign("XML: Failed to parse document for ARGS.");
ms_dbg_a(m_transaction, 4, "XML: Failed to parse document for ARGS.");
return false;
Expand Down Expand Up @@ -326,7 +327,7 @@ bool XML::complete(std::string *error) {
ms_dbg_a(m_transaction, 4, "XML: Parsing complete (well_formed " \
+ std::to_string(m_data.well_formed) + ").");

if (m_data.well_formed != 1) {
if (m_require_well_formed && m_data.well_formed != 1) {
error->assign("XML: Failed to parse document.");
ms_dbg_a(m_transaction, 4, "XML: Failed to parse document.");
return false;
Expand Down
3 changes: 2 additions & 1 deletion src/request_body_processor/xml.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class XML {
public:
explicit XML(Transaction *transaction);
~XML();
bool init();
bool init(bool require_well_formed = true);
bool processChunk(const char *buf, unsigned int size, std::string *err);
bool complete(std::string *err);
static xmlParserInputBufferPtr unloadExternalEntity(const char *URI,
Expand All @@ -98,6 +98,7 @@ class XML {
private:
Transaction *m_transaction;
std::string m_header;
bool m_require_well_formed;
};

#endif
Expand Down
Loading
Loading