From 5d5e10bfde20526b4fc528fe5ef8593b6ccc6f27 Mon Sep 17 00:00:00 2001 From: Felipe Zimmerle Date: Fri, 17 Jul 2015 15:12:15 -0300 Subject: [PATCH] Adds support for basic Multipart process Adjustments will be needed, for instance: the logging support is still missing --- headers/modsecurity/assay.h | 27 +++ src/Makefile.am | 2 + src/assay.cc | 48 +++- src/parser/seclang-scanner.ll | 2 +- src/request_body_processor/multipart.cc | 221 +++++++++++++++++++ src/request_body_processor/multipart.h | 52 +++++ src/request_body_processor/multipart_blob.cc | 121 ++++++++++ src/request_body_processor/multipart_blob.h | 51 +++++ 8 files changed, 518 insertions(+), 6 deletions(-) create mode 100644 src/request_body_processor/multipart.cc create mode 100644 src/request_body_processor/multipart.h create mode 100644 src/request_body_processor/multipart_blob.cc create mode 100644 src/request_body_processor/multipart_blob.h diff --git a/headers/modsecurity/assay.h b/headers/modsecurity/assay.h index 073c5927..a7e6ca19 100644 --- a/headers/modsecurity/assay.h +++ b/headers/modsecurity/assay.h @@ -110,6 +110,32 @@ class Assay { int processURI(const char *uri, const char *protocol, const char *http_version); + /** + * Types of request body that ModSecurity may give a special treatment + * for the data. + */ + enum RequestBodyType { + /** + * + */ + UnknownFormat, + /** + * + */ + MultiPartRequestBody, + /** + * + */ + WWWFormUrlEncoded, + /** + * + */ + JSONRequestBody, + /** + * + */ + XMLRequestBody + }; int processRequestHeaders(); int addRequestHeader(const std::string& key, const std::string& value); @@ -185,6 +211,7 @@ class Assay { double m_ARGScombinedSize; /** TODO: Support to save double in the storage. */ std::string *m_ARGScombinedSizeStr; + RequestBodyType m_requestBodyType; std::ostringstream m_requestBody; std::ostringstream m_responseBody; diff --git a/src/Makefile.am b/src/Makefile.am index 5efceed0..471a1ae1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -91,6 +91,8 @@ libmodsecurity_la_SOURCES = \ rules.cc \ utils.cc \ debug_log.cc \ + request_body_processor/multipart.cc \ + request_body_processor/multipart_blob.cc \ rule.cc \ unique_id.cc \ variable.cc \ diff --git a/src/assay.cc b/src/assay.cc index 22f8531d..dabd3854 100644 --- a/src/assay.cc +++ b/src/assay.cc @@ -34,8 +34,10 @@ #include "src/utils.h" #include "src/audit_log.h" #include "src/unique_id.h" +#include "request_body_processor/multipart.h" using ModSecurity::actions::Action; +using ModSecurity::RequestBodyProcessor::Multipart; namespace ModSecurity { @@ -95,6 +97,7 @@ Assay::Assay(ModSecurity *ms, Rules *rules) m_namesArgs(NULL), m_namesArgsPost(NULL), m_namesArgsGet(NULL), + m_requestBodyType(UnknownFormat), start(std::chrono::system_clock::now()), m_ms(ms) { id = std::to_string(this->timeStamp) + \ @@ -271,7 +274,7 @@ int Assay::processURI(const char *uri, const char *protocol, */ int Assay::processRequestHeaders() { debug(4, "Starting phase REQUEST_HEADERS. (SecRules 1)"); - this->m_rules->evaluate(ModSecurity::RequestHeadersPhase, this); + this->m_rules->evaluate(ModSecurity::RequestHeadersPhase, this); return 0; } @@ -311,6 +314,25 @@ int Assay::addRequestHeader(const std::string& key, this->store_variable("AUTH_TYPE", type[0]); } + /** + * Simple check to decide the request body content. This is not the right + * place, the "body processor" should be able to tell what he is capable + * to deal with. + * + */ + + if (tolower(key) == "content-type") { + std::string multipart("multipart/form-data"); + std::string l = tolower(value); + + if (l.compare(0, multipart.length(), multipart) == 0) { + this->m_requestBodyType = MultiPartRequestBody; + } + + if (l == "application/x-www-form-urlencoded") { + this->m_requestBodyType = WWWFormUrlEncoded; + } + } return 1; } @@ -391,7 +413,26 @@ int Assay::addRequestHeader(const unsigned char *key, size_t key_n, int Assay::processRequestBody() { debug(4, "Starting phase REQUEST_BODY. (SecRules 2)"); - if (m_requestBody.tellp() > 0) { + if (m_requestBody.tellp() <= 0) { + return true; + } + + if (m_requestBodyType == MultiPartRequestBody) { + std::string *a = resolve_variable_first("REQUEST_HEADERS:Content-Type"); + if (a != NULL) { + Multipart m(*a); + m.init(); + m.process(m_requestBody.str()); + for (auto &a : m.variables) { + store_variable(a.first, a.second); + } + } + } + + if (m_requestBodyType == WWWFormUrlEncoded) { + std::string content = uri_decode(m_requestBody.str()); + content.erase(content.length()-1, 1); + /** * FIXME: * @@ -399,9 +440,6 @@ int Assay::processRequestBody() { * the secrules said about it. * */ - std::string content = uri_decode(m_requestBody.str()); - content.erase(content.length()-1, 1); - char sep1 = '&'; const char *pos = strchr(content.c_str(), '?'); diff --git a/src/parser/seclang-scanner.ll b/src/parser/seclang-scanner.ll index 19d088bc..d56d955e 100755 --- a/src/parser/seclang-scanner.ll +++ b/src/parser/seclang-scanner.ll @@ -56,7 +56,7 @@ OPERATORNOARG (?i:@detectSQLi|@detectXSS|@geoLookup|@validateUrlEncoding|@valida TRANSFORMATION t:(lowercase|urlDecodeUni|urlDecode|none|compressWhitespace|removeWhitespace|replaceNulls|removeNulls|htmlEntityDecode|jsDecode|cssDecode|trim) -VARIABLE (?i:AUTH_TYPE|ARGS_NAMES|ARGS|QUERY_STRING|REMOTE_ADDR|REQUEST_BASENAME|REQUEST_BODY|REQUEST_COOKIES_NAMES|REQUEST_COOKIES|REQUEST_FILENAME|REQUEST_HEADERS_NAMES|REQUEST_HEADERS|REQUEST_METHOD|REQUEST_PROTOCOL|REQUEST_URI|RESPONSE_BODY|RESPONSE_CONTENT_LENGTH|RESPONSE_CONTENT_TYPE|RESPONSE_HEADERS_NAMES|RESPONSE_HEADERS|RESPONSE_PROTOCOL|RESPONSE_STATUS|TX) +VARIABLE (?i:FILES|AUTH_TYPE|ARGS_NAMES|ARGS|QUERY_STRING|REMOTE_ADDR|REQUEST_BASENAME|REQUEST_BODY|REQUEST_COOKIES_NAMES|REQUEST_COOKIES|REQUEST_FILENAME|REQUEST_HEADERS_NAMES|REQUEST_HEADERS|REQUEST_METHOD|REQUEST_PROTOCOL|REQUEST_URI|RESPONSE_BODY|RESPONSE_CONTENT_LENGTH|RESPONSE_CONTENT_TYPE|RESPONSE_HEADERS_NAMES|RESPONSE_HEADERS|RESPONSE_PROTOCOL|RESPONSE_STATUS|TX) RUN_TIME_VAR_DUR (?i:DURATION) RUN_TIME_VAR_ENV (?i:ENV) diff --git a/src/request_body_processor/multipart.cc b/src/request_body_processor/multipart.cc new file mode 100644 index 00000000..1fed765f --- /dev/null +++ b/src/request_body_processor/multipart.cc @@ -0,0 +1,221 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 Trustwave Holdings, Inc. (http://www.trustwave.com/) + * + * You may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * If any of the files related to licensing are missing or if you have any + * other questions related to licensing please contact Trustwave Holdings, Inc. + * directly using the email address security@modsecurity.org. + * + */ + +#include "request_body_processor/multipart.h" + +#include +#include +#include + +#include "request_body_processor/multipart_blob.h" + +namespace ModSecurity { +namespace RequestBodyProcessor { + +Multipart::Multipart(std:: string header) + : m_boundaryStartsWithWhiteSpace(false), + m_boundaryIsQuoted(false), + m_header(header) { +} + + +bool Multipart::init() { + if (m_header.length() > 1024) { + debug(4, "Multipart: Invalid boundary in Content-Type (length)."); + return false; + } + + std::size_t boundary_pos = m_header.find("boundary"); + if (boundary_pos < 0) { + if (boundary_pos > 0 && m_header.find("boundary", boundary_pos) > 0) { + debug(4, "Multipart: Multiple boundary parameters in " \ + "Content-Type."); + return false; + } + } + + std::string boundary = m_header.c_str() + boundary_pos; + std::size_t semicolon_pos = boundary.find(";"); + if (semicolon_pos != std::string::npos + && boundary.find(";", semicolon_pos + 1) != std::string::npos) { + debug(4, "Multipart: Invalid boundary in Content-Type. (malformed). " \ + "Too many semicolons."); + return false; + } + + if (semicolon_pos < 0) { + debug(4, "Multipart: Missing semicolon."); + } + + if (boundary.at(8) != '=') { + debug(4, "Multipart: Invalid boundary in Content-Type. (malformed). " \ + "Missing equals."); + return false; + } +#if 0 + Not checked. + /* Check parameter name ends well. */ + if (b != (msr->mpd->boundary + 8)) { + /* Check all characters between the end of the boundary + * and the = character. + */ + for (p = msr->mpd->boundary + 8; p < b; p++) { + if (isspace(*p)) { + /* Flag for whitespace after parameter name. */ + msr->mpd->flag_boundary_whitespace = 1; + } else { + msr->mpd->flag_error = 1; + *error_msg = apr_psprintf(msr->mp, "Multipart: " \ + "Invalid boundary in C-T (parameter name)."); + return -1; + } + } + } + + b++; /* Go over the = character. */ + len = strlen(b); + + /* Flag for whitespace before parameter value. */ + if (isspace(*b)) { + msr->mpd->flag_boundary_whitespace = 1; + } +#endif // if 0 + if (boundary.at(8 + 1) == ' ') { + m_boundaryStartsWithWhiteSpace = true; + debug(4, "Multipart: Boundary starts with a white space"); + } + + if ((m_boundaryStartsWithWhiteSpace && boundary.at(8 + 2) == '"') || + (!m_boundaryStartsWithWhiteSpace && boundary.at(8 + 1) == '"')) { + m_boundaryIsQuoted = true; + debug(4, "Multipart: Boundary inside quotes"); + } + + if (m_boundaryIsQuoted && boundary.at(boundary.length()-1) != '"') { + debug(4, "Multipart: Invalid boundary in Content-type (quote)."); + return false; + } + +#if 0 + Not checking + /* Case-insensitive test for the string "boundary" in the boundary. */ + if (count_boundary_params(msr->mp, msr->mpd->boundary) != 0) { + msr->mpd->flag_error = 1; + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid boundary " \ + "in C-T (content)."); + return -1; + } +#endif // if 0 + + int real_boundary_pos = 9; + + if (m_boundaryStartsWithWhiteSpace) { + real_boundary_pos++; + } + + if (m_boundaryIsQuoted) { + real_boundary_pos++; + } + + m_boundary = boundary.c_str() + real_boundary_pos; + + if (m_boundaryIsQuoted) { + m_boundary.pop_back(); + } + + if (boundaryContainsOnlyValidCharacters() == false) { + debug(4, "Multipart: Invalid boundary in Content-type " \ + "(invalid characters)."); + return false; + } +} + + +bool Multipart::boundaryContainsOnlyValidCharacters() { + if (m_boundary.empty()) { + return false; + } + + for (int i = 0; i < m_boundary.length(); i++) { + int c = m_boundary.at(i); + + /* Control characters and space not allowed. */ + /* Non-ASCII characters not allowed. */ + if (c < 32 || c > 126) { + return false; + } + + switch (c) { + /* Special characters not allowed. */ + case '(' : + case ')' : + case '<' : + case '>' : + case '@' : + case ',' : + case ';' : + case ':' : + case '\\' : + case '"' : + case '/' : + case '[' : + case ']' : + case '?' : + case '=' : + return false; + break; + default : + break; + } + } + + return true; +} + +bool Multipart::process(std::string data) { + std::list blobs; + size_t start = data.find(m_boundary); + size_t endl = 1; + + if (start != 0) { + debug(4, "Multipart: Boundary was not the first thing."); + } + + while (start != std::string::npos) { + size_t end = data.find(m_boundary, start + m_boundary.length()); + if (end == std::string::npos) { + start = end; + continue; + } + std::string block = std::string(data, start + m_boundary.length() + + + endl, end - (start + m_boundary.length() + endl) - endl); + blobs.push_back(block); + start = end; + } + + for (std::string x : blobs) { + MultipartBlob m(x, this); + + if (m.filename.empty() == false) { + variables.emplace("FILES:" + m.name, m.filename); + } + } + + return true; +} + + +} // namespace RequestBodyProcessor +} // namespace ModSecurity diff --git a/src/request_body_processor/multipart.h b/src/request_body_processor/multipart.h new file mode 100644 index 00000000..51b6d729 --- /dev/null +++ b/src/request_body_processor/multipart.h @@ -0,0 +1,52 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 Trustwave Holdings, Inc. (http://www.trustwave.com/) + * + * You may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * If any of the files related to licensing are missing or if you have any + * other questions related to licensing please contact Trustwave Holdings, Inc. + * directly using the email address security@modsecurity.org. + * + */ + +#include +#include + + +#ifndef SRC_REQUEST_BODY_PROCESSOR_MULTIPART_H_ +#define SRC_REQUEST_BODY_PROCESSOR_MULTIPART_H_ + +#include "modsecurity/assay.h" + +namespace ModSecurity { +namespace RequestBodyProcessor { + +class Multipart { + public: + explicit Multipart(std::string header); + bool init(); + + bool boundaryContainsOnlyValidCharacters(); + bool conuntBoundaryParameters(); + bool process(std::string data); + + ModSecurityStringVariables variables; + private: + void debug(int a, std::string str) { + std::cout << "Debug: " << str << std::endl; + } + std::string m_boundary; + std::string m_header; + + bool m_boundaryStartsWithWhiteSpace = false; + bool m_boundaryIsQuoted = false; +}; + +} // namespace RequestBodyProcessor +} // namespace ModSecurity + +#endif // SRC_REQUEST_BODY_PROCESSOR_MULTIPART_H_ diff --git a/src/request_body_processor/multipart_blob.cc b/src/request_body_processor/multipart_blob.cc new file mode 100644 index 00000000..1d6e8213 --- /dev/null +++ b/src/request_body_processor/multipart_blob.cc @@ -0,0 +1,121 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 Trustwave Holdings, Inc. (http://www.trustwave.com/) + * + * You may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * If any of the files related to licensing are missing or if you have any + * other questions related to licensing please contact Trustwave Holdings, Inc. + * directly using the email address security@modsecurity.org. + * + */ + +#include "request_body_processor/multipart_blob.h" + +#include +#include +#include + + +namespace ModSecurity { +namespace RequestBodyProcessor { + +MultipartBlob::MultipartBlob(const std::string &blob, Multipart *parent) + : m_blob(blob), + m_parent(parent) { + processContent(); +} + + +bool MultipartBlob::processContent() { + size_t end = 0; + size_t offset = 0; + + end = m_blob.find("\n", offset); + if (end == std::string::npos) { + debug(4, "Missing end of line"); + return false; + } + std::string firstLine = std::string(m_blob, offset, end); + + offset = end + 1; + end = m_blob.find("\n", offset); + if (end == std::string::npos) { + debug(4, "Missing end of line"); + return false; + } + std::string secondLine = std::string(m_blob, offset, end - offset); + + bool dispositionLine = processContentDispositionLine(firstLine); + if (dispositionLine == false) { + return false; + } + + bool contentTypeLine = processContentTypeLine(secondLine); + if (dispositionLine == false) { + return false; + } + + offset = end + 1; + if (contentType.empty() == false) { + end = m_blob.find_first_of("\n", offset); + if (end == std::string::npos) { + debug(4, "Missing end of line"); + return false; + } + offset = end + 1; + } + content = std::string(m_blob, offset, m_blob.length() - offset + 1); +} + + +bool MultipartBlob::processContentTypeLine( + const std::string &contentTypeLine) { + size_t contentTypeKeyLength = 14; + + if (contentTypeLine.length() <= contentTypeKeyLength) { + return true; + } + + contentType = std::string(contentTypeLine, contentTypeKeyLength, + contentTypeLine.length() - contentTypeKeyLength); + return true; +} + + +bool MultipartBlob::processContentDispositionLine( + const std::string &dispositionLine) { + size_t offset; + + if (dispositionLine.compare(21, 9, "form-data") != 0) { + debug(4, "Multipart: Content-Disposition is unknown"); + return false; + } + + // Find name= + offset = dispositionLine.find("name="); + if (offset != std::string::npos) { + offset = offset + 5 /* name= */ + 1 /* " */; + size_t end = dispositionLine.find("\"", offset); + if (end != std::string::npos) { + name = std::string(dispositionLine, offset, end - offset); + } + } + + // Find filename= + offset = dispositionLine.find("filename="); + if (offset != std::string::npos) { + offset = offset + 9 /* filename= */ + 1 /* " */; + size_t end = dispositionLine.find("\"", offset); + if (end != std::string::npos) { + filename = std::string(dispositionLine, offset, end - offset); + } + } +} + + +} // namespace RequestBodyProcessor +} // namespace ModSecurity diff --git a/src/request_body_processor/multipart_blob.h b/src/request_body_processor/multipart_blob.h new file mode 100644 index 00000000..c4a9e0a1 --- /dev/null +++ b/src/request_body_processor/multipart_blob.h @@ -0,0 +1,51 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 Trustwave Holdings, Inc. (http://www.trustwave.com/) + * + * You may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * If any of the files related to licensing are missing or if you have any + * other questions related to licensing please contact Trustwave Holdings, Inc. + * directly using the email address security@modsecurity.org. + * + */ + +#include +#include + +#include "request_body_processor/multipart.h" + +#ifndef SRC_REQUEST_BODY_PROCESSOR_MULTIPART_BLOB_H_ +#define SRC_REQUEST_BODY_PROCESSOR_MULTIPART_BLOB_H_ + +namespace ModSecurity { +namespace RequestBodyProcessor { + +class MultipartBlob { + public: + explicit MultipartBlob(const std::string &blob, Multipart *parent); + + bool processContent(); + bool processContentDispositionLine(const std::string &dispositionLine); + bool processContentTypeLine(const std::string &contentTypeLine); + + void debug(int a, std::string str) { + std::cout << "Debug: " << str << std::endl; + } + + std::string name; + std::string filename; + std::string contentType; + std::string content; + private: + const std::string m_blob; + Multipart *m_parent; +}; + +} // namespace RequestBodyProcessor +} // namespace ModSecurity + +#endif // SRC_REQUEST_BODY_PROCESSOR_MULTIPART_BLOB_H_