Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
132 changes: 126 additions & 6 deletions lib/http/HttpClient_Apple.mm
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,111 @@
#include "utils/StringUtils.hpp"
#include "utils/Utils.hpp"

// Streams the response body in bounded chunks and enforces MAX_HTTP_RESPONSE_SIZE.
// The completionHandler-based NSURLSession APIs fully materialize the response body
// as an NSData before handing it over, so an attacker-controlled collector could force
// a large allocation. This delegate instead accumulates data incrementally in
// didReceiveData: and cancels the transfer as soon as the cap would be exceeded, so no
// more than the cap is ever buffered. Delegate callbacks may arrive on the session's
// delegate queue while a request thread registers a task, so shared state is guarded.
@interface MATStreamingSessionDelegate : NSObject <NSURLSessionDataDelegate>
- (void)registerTask:(NSURLSessionTask*)task
handler:(void (^)(NSData* data, NSURLResponse* response, NSError* error))handler;
@end

@implementation MATStreamingSessionDelegate {
NSMutableDictionary<NSNumber*, NSMutableData*>* _buffers;
NSMutableDictionary<NSNumber*, id>* _handlers;
NSMutableSet<NSNumber*>* _overCap;
}

- (instancetype)init
{
self = [super init];
if (self)
{
_buffers = [NSMutableDictionary new];
_handlers = [NSMutableDictionary new];
_overCap = [NSMutableSet new];
}
return self;
}

- (void)registerTask:(NSURLSessionTask*)task
handler:(void (^)(NSData*, NSURLResponse*, NSError*))handler
{
NSNumber* key = @(task.taskIdentifier);
@synchronized(self)
{
_buffers[key] = [NSMutableData new];
_handlers[key] = [handler copy];
}
}

- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveData:(NSData*)data
{
NSNumber* key = @(dataTask.taskIdentifier);
@synchronized(self)
{
if ([_overCap containsObject:key])
{
return;
}
NSMutableData* buffer = _buffers[key];
if (buffer == nil)
{
return;
}
if (buffer.length + data.length > MAT::MAX_HTTP_RESPONSE_SIZE)
{
// Refuse the over-large response: stop buffering and cancel the transfer.
[_overCap addObject:key];
[dataTask cancel];
return;
}
[buffer appendData:data];
}
}

- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error
{
NSNumber* key = @(task.taskIdentifier);
void (^handler)(NSData*, NSURLResponse*, NSError*) = nil;
NSData* body = nil;
BOOL overCap = NO;
@synchronized(self)
{
handler = (void (^)(NSData*, NSURLResponse*, NSError*))_handlers[key];
body = _buffers[key];
overCap = [_overCap containsObject:key];
[_handlers removeObjectForKey:key];
[_buffers removeObjectForKey:key];
[_overCap removeObject:key];
}
if (handler == nil)
{
return;
}
if (overCap)
{
// Surface a non-cancellation error so the request maps to NetworkFailure
// (retried), not Aborted (which is reserved for caller-initiated cancels).
NSError* capError = [NSError errorWithDomain:@"MATResponseCap"
code:-1
userInfo:@{ NSLocalizedDescriptionKey : @"HTTP response exceeds max buffered size" }];
handler(nil, task.response, capError);
}
else
{
handler(body, task.response, error);
}
}
@end

namespace MAT_NS_BEGIN {

static std::string NextReqId()
Expand All @@ -31,6 +136,7 @@

static dispatch_once_t once;
static NSURLSession* session;
static MATStreamingSessionDelegate* sessionDelegate;

class HttpRequestApple : public SimpleHttpRequest
{
Expand All @@ -42,7 +148,10 @@
m_parent->Add(static_cast<IHttpRequest*>(this));
dispatch_once(&once, ^{
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
session = [NSURLSession sessionWithConfiguration:sessionConfig];
sessionDelegate = [MATStreamingSessionDelegate new];
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:sessionDelegate
delegateQueue:nil];
});
}

Expand Down Expand Up @@ -75,15 +184,18 @@ void SendAsync(IHttpResponseCallback* callback)
if(equalsIgnoreCase(m_method, "get"))
{
[m_urlRequest setHTTPMethod:@"GET"];
m_dataTask = [session dataTaskWithRequest:m_urlRequest completionHandler:m_completionMethod];
m_dataTask = [session dataTaskWithRequest:m_urlRequest];
}
else
{
[m_urlRequest setHTTPMethod:@"POST"];
NSData* postData = [NSData dataWithBytes:m_body.data() length:m_body.size()];
m_dataTask = [session uploadTaskWithRequest:m_urlRequest fromData:postData completionHandler:m_completionMethod];
m_dataTask = [session uploadTaskWithRequest:m_urlRequest fromData:postData];
}

// Register before resume so the streaming delegate has the buffer and
// completion handler in place before any response data arrives.
[sessionDelegate registerTask:m_dataTask handler:m_completionMethod];
[m_dataTask resume];
}
}
Expand Down Expand Up @@ -120,10 +232,18 @@ void HandleResponse(NSData* data, NSURLResponse* response, NSError* error)
}
else
{
// The streaming delegate has already enforced MAX_HTTP_RESPONSE_SIZE
// (an over-cap response arrives here as a cap error, handled above), so
// data is bounded. Guard against a nil/empty body to avoid pointer
// arithmetic on a null [data bytes].
simpleResponse->m_result = HttpResult_OK;
auto body = static_cast<const uint8_t*>([data bytes]);
simpleResponse->m_body.reserve(data.length);
std::copy(body, body + data.length, std::back_inserter(simpleResponse->m_body));
const size_t length = static_cast<size_t>(data.length);
if (length > 0)
{
auto body = static_cast<const uint8_t*>([data bytes]);
simpleResponse->m_body.reserve(length);
std::copy(body, body + length, std::back_inserter(simpleResponse->m_body));
}
}
m_callback->OnHttpResponse(simpleResponse);
}
Expand Down
52 changes: 33 additions & 19 deletions lib/http/HttpClient_WinInet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,27 +324,41 @@ class WinInetRequestWrapper
// It might potentially be another async operation which will
// trigger INTERNET_STATUS_REQUEST_COMPLETE again.

m_bodyBuffer.insert(m_bodyBuffer.end(), m_buffer, m_buffer + m_bufferUsed);
while (!m_readingData || m_bufferUsed != 0) {
BOOL bResult = ::InternetReadFile(m_hWinInetRequest, m_buffer, sizeof(m_buffer), &m_bufferUsed);
m_readingData = true;
if (!bResult) {
dwError = GetLastError();
if (dwError == ERROR_IO_PENDING) {
// Do not touch anything from this thread anymore.
// The buffer passed to InternetReadFile() and the
// read count will be filled asynchronously, so they
// must stay valid and writable until the next
// INTERNET_STATUS_REQUEST_COMPLETE callback comes
// (that's why those are member variables).
LOG_TRACE("InternetReadFile() failed: ERROR_IO_PENDING. Waiting for INTERNET_STATUS_REQUEST_COMPLETE to be called again");
return;
// SECURITY: refuse an over-large response instead of buffering it (see
// MAX_HTTP_RESPONSE_SIZE) so a hostile/MITM'd collector cannot exhaust
// process memory. Checked before every append so the buffer never exceeds
// the cap; reported as an invalid server response -> NetworkFailure (retried).
if (m_bodyBuffer.size() + m_bufferUsed > MAX_HTTP_RESPONSE_SIZE) {
LOG_WARN("HTTP response exceeds max buffered size (%zu bytes); aborting", MAX_HTTP_RESPONSE_SIZE);
dwError = ERROR_HTTP_INVALID_SERVER_RESPONSE;
} else {
m_bodyBuffer.insert(m_bodyBuffer.end(), m_buffer, m_buffer + m_bufferUsed);
while (!m_readingData || m_bufferUsed != 0) {
BOOL bResult = ::InternetReadFile(m_hWinInetRequest, m_buffer, sizeof(m_buffer), &m_bufferUsed);
m_readingData = true;
if (!bResult) {
dwError = GetLastError();
if (dwError == ERROR_IO_PENDING) {
// Do not touch anything from this thread anymore.
// The buffer passed to InternetReadFile() and the
// read count will be filled asynchronously, so they
// must stay valid and writable until the next
// INTERNET_STATUS_REQUEST_COMPLETE callback comes
// (that's why those are member variables).
LOG_TRACE("InternetReadFile() failed: ERROR_IO_PENDING. Waiting for INTERNET_STATUS_REQUEST_COMPLETE to be called again");
return;
}
LOG_WARN("InternetReadFile() failed: %d", dwError);
break;
}
LOG_WARN("InternetReadFile() failed: %d", dwError);
break;
}

m_bodyBuffer.insert(m_bodyBuffer.end(), m_buffer, m_buffer + m_bufferUsed);
if (m_bodyBuffer.size() + m_bufferUsed > MAX_HTTP_RESPONSE_SIZE) {
LOG_WARN("HTTP response exceeds max buffered size (%zu bytes); aborting", MAX_HTTP_RESPONSE_SIZE);
dwError = ERROR_HTTP_INVALID_SERVER_RESPONSE;
break;
}
m_bodyBuffer.insert(m_bodyBuffer.end(), m_buffer, m_buffer + m_bufferUsed);
}
}
}

Expand Down
109 changes: 85 additions & 24 deletions lib/http/HttpClient_WinRt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ namespace MAT_NS_BEGIN {

void SendHttpAsyncRequest(HttpRequestMessage ^req)
{
IAsyncOperationWithProgress<HttpResponseMessage^, HttpProgress>^ operation = m_parent.getHttpClient()->SendRequestAsync(req, HttpCompletionOption::ResponseContentRead);
IAsyncOperationWithProgress<HttpResponseMessage^, HttpProgress>^ operation = m_parent.getHttpClient()->SendRequestAsync(req, HttpCompletionOption::ResponseHeadersRead);
m_cancellationTokenSource = cancellation_token_source();

create_task(operation, m_cancellationTokenSource.get_token()).
Expand Down Expand Up @@ -202,37 +202,98 @@ namespace MAT_NS_BEGIN {
index++;
}

auto operation = m_httpResponseMessage->Content->ReadAsBufferAsync();
auto task = create_task(operation);
if (task.wait() == task_status::completed)
// Read content headers before streaming the body.
IMapView<String^, String^>^ contentHeadersView = m_httpResponseMessage->Content->Headers->GetView();
auto contentHeadersiterator = contentHeadersView->First();
unsigned int contentHeadersIndex = 0;
while (contentHeadersIndex < contentHeadersView->Size)
{
IMapView<String^, String^>^ contentHeadersView = m_httpResponseMessage->Content->Headers->GetView();
String^ Key = contentHeadersiterator->Current->Key;
String^ Value = contentHeadersiterator->Current->Value;

auto contentHeadersiterator = contentHeadersView->First();
unsigned int contentHeadersIndex = 0;
while (contentHeadersIndex < contentHeadersView->Size)
{
String^ Key = contentHeadersiterator->Current->Key;
String^ Value = contentHeadersiterator->Current->Value;
response->m_headers.add(from_platform_string(Key), from_platform_string(Value));
contentHeadersiterator->MoveNext();
contentHeadersIndex++;
}

response->m_headers.add(from_platform_string(Key), from_platform_string(Value));
contentHeadersiterator->MoveNext();
contentHeadersIndex++;
// SECURITY: stream the body in bounded chunks and enforce
// MAX_HTTP_RESPONSE_SIZE. SendRequestAsync uses ResponseHeadersRead, so
// the framework does not pre-buffer the whole body; reading it here in
// chunks ensures an oversized response is never fully materialized in
// memory (a hostile/MITM'd collector cannot exhaust process memory).
// task::wait()/get() rethrow if a read faults, so guard the whole stream.
try
{
IInputStream^ inputStream = nullptr;
{
auto streamOp = m_httpResponseMessage->Content->ReadAsInputStreamAsync();
auto streamTask = create_task(streamOp, m_cancellationTokenSource.get_token());
auto status = streamTask.wait();
if (status == task_status::completed)
{
inputStream = streamTask.get();
}
else
{
// Caller-initiated cancel maps to Aborted; anything else is a failure.
response->m_result = (status == task_status::canceled) ? HttpResult_Aborted : HttpResult_NetworkFailure;
}
}

auto buffer = task.get();
size_t length = buffer->Length;

if (length > 0)
if (inputStream != nullptr)
{
response->m_body.reserve(length);
response->m_body.resize(length);
DataReader^ dataReader = DataReader::FromBuffer(buffer);
dataReader->ReadBytes((Platform::ArrayReference<unsigned char>(reinterpret_cast<unsigned char*>(response->m_body.data()), (DWORD)length)));
dataReader->DetachBuffer();
delete dataReader;
const unsigned int chunkSize = 64 * 1024;
for (;;)
{
Buffer^ chunk = ref new Buffer(chunkSize);
auto readOp = inputStream->ReadAsync(chunk, chunkSize, InputStreamOptions::Partial);
auto readTask = create_task(readOp, m_cancellationTokenSource.get_token());
auto status = readTask.wait();
if (status != task_status::completed)
{
// Drop any partial body; caller cancel -> Aborted, else failure.
response->m_result = (status == task_status::canceled) ? HttpResult_Aborted : HttpResult_NetworkFailure;
response->m_body.clear();
break;
}

IBuffer^ readBuffer = readTask.get();
unsigned int readLength = (readBuffer != nullptr) ? readBuffer->Length : 0;
if (readLength == 0)
{
break; // end of stream
}

if (response->m_body.size() + readLength > MAX_HTTP_RESPONSE_SIZE)
{
LOG_WARN("HTTP response exceeds max buffered size (%zu bytes); aborting", MAX_HTTP_RESPONSE_SIZE);
response->m_result = HttpResult_NetworkFailure;
response->m_body.clear();
break;
}

const size_t oldSize = response->m_body.size();
response->m_body.resize(oldSize + readLength);
DataReader^ dataReader = DataReader::FromBuffer(readBuffer);
dataReader->ReadBytes((Platform::ArrayReference<unsigned char>(reinterpret_cast<unsigned char*>(response->m_body.data() + oldSize), readLength)));
dataReader->DetachBuffer();
delete dataReader;
}
delete inputStream;
}
}
catch (Platform::Exception^ ex)
{
// A faulted read rethrows here; drop any partial body and fail the request.
LOG_WARN("Reading HTTP response body failed: 0x%08x", ex->HResult);
response->m_result = HttpResult_NetworkFailure;
response->m_body.clear();
}
catch (...)
{
response->m_result = HttpResult_NetworkFailure;
response->m_body.clear();
}
}
else
{
Expand Down
13 changes: 13 additions & 0 deletions lib/include/public/IHttpClient.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,24 @@
#include <map>
#include <string>
#include <vector>
#include <cstddef>

///@cond INTERNAL_DOCS
namespace MAT_NS_BEGIN
{
class ILogConfiguration;

/// <summary>
/// SECURITY: upper bound (in bytes) on an HTTP response body that a transport
/// will buffer. OneCollector protocol responses are small (status, kill-switch
/// tokens, retry-after, small config), so this generous cap never rejects a
/// legitimate response, but it stops a hostile or MITM'd collector from driving
/// unbounded memory growth by returning an oversized body (a memory-amplification
/// DoS of the embedding process). A transport that would exceed it refuses the
/// response and reports the request as a network failure so it is retried.
/// </summary>
static constexpr std::size_t MAX_HTTP_RESPONSE_SIZE = 16u * 1024u * 1024u; // 16 MB

/// <summary>
/// The HttpHeaders class contains a set of HTTP headers.
/// </summary>
Expand Down
Loading