Skip to content

Commit 717c9cb

Browse files
committed
Fix OOB write on URL callback with 2GB+ response. Add new size limit.
The OOB write did not happen on file-backed downloads, such as remote includes. It only happened for memory-backed requests, which are only these 4 in standard UnrealIRCd: centralblocklist, central spam report, other spamreport blocks (eg to dronebl) and the log block with destination webhook. All those 4 cases are very likely to be trusted web servers, given the nature of the data you are sending to them. The fix was to extend the size fields everywhere to 64 bits. It was applied to both URL backends: url_unreal.c and url_curl.c. The new API feature is a 'max_size' in OutgoingWebRequest, which defaults to 1MB. This is only used for memory-backed responses, so not for real file downloads. This fixes not only the reported bug but also the case where a rogue webserver was unbounded in terms of what response it could send back, potentially filling up gigabytes of server memory. Reported by Link420.
1 parent abbbcd1 commit 717c9cb

6 files changed

Lines changed: 68 additions & 18 deletions

File tree

include/config.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,13 @@
210210
*/
211211
#define DOWNLOAD_MAX_REDIRECTS 2
212212

213+
/* Default maximum size (in bytes) for memory-backed HTTP responses
214+
* (i.e. when OutgoingWebRequest.store_in_file == 0). Responses exceeding
215+
* this are rejected and the transfer is aborted. Callers can override
216+
* by setting OutgoingWebRequest.max_size before url_start_async().
217+
*/
218+
#define DOWNLOAD_MAX_SIZE 1048576
219+
213220
/*
214221
* Max time from the nickname change that still causes KILL
215222
* automaticly to switch for the current nick of that user. (seconds)

include/h.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ extern int valid_operclass_name(const char *str);
15611561
#define safe_free_outgoingwebrequest(x) do { if (x) { free_outgoingwebrequest(x); x = NULL; } } while(0)
15621562
extern void free_outgoingwebrequest(OutgoingWebRequest *r);
15631563
extern OutgoingWebRequest *duplicate_outgoingwebrequest(OutgoingWebRequest *orig);
1564-
extern void url_callback(OutgoingWebRequest *r, const char *file, const char *memory, int memory_len, const char *errorbuf, int cached, void *ptr);
1564+
extern void url_callback(OutgoingWebRequest *r, const char *file, const char *memory, long long memory_len, const char *errorbuf, int cached, void *ptr);
15651565
extern const char *synchronous_http_request(const char *url, int max_redirects, int connect_timeout, int transfer_timeout);
15661566
extern int update_known_user_cache(Client *client);
15671567
extern MODVAR SecurityGroup *known_users;

include/struct.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,8 @@ struct OutgoingWebRequest
19831983
int connect_timeout; /**< How many seconds to wait for the (TLS) connect to succeed */
19841984
int transfer_timeout; /**< How many seconds the total transfer may take (connect+reading everything) */
19851985
int minimum_tls_version;
1986+
long long max_size; /**< Max response size for memory-backed downloads, in bytes.
1987+
* 0 = use DOWNLOAD_MAX_SIZE. Ignored for file-backed. */
19861988
// If you are adding fields here:
19871989
// 1) update duplicate_outgoingwebrequest() in src/misc.c
19881990
// 2) and update free_outgoingwebrequest() there as well (if something needs to be freed)
@@ -1993,7 +1995,7 @@ struct OutgoingWebResponse
19931995
{
19941996
const char *file; /**< The temporary file of the download, or NULL. This is only set if OutgoingWebRequest had 'store_in_file' set to 1 and the download was succesful. */
19951997
const char *memory; /**< The memory buffer of the response, or NULL if an error occured (see errorbuf) */
1996-
int memory_len; /**< The length of 'memory', since the response may contain binary data. */
1998+
long long memory_len; /**< The length of 'memory', since the response may contain binary data. */
19971999
const char *errorbuf; /**< If this is non-NULL then an error occured and this is the error string. Check this member before checking any others! */
19982000
int cached; /**< Set to 1 if OutgoingWebRequest had 'cachetime' set and we have a cache hit on the webserver. The file and errobuf will be NULL since there was no data transfer. */
19992001
void *ptr; /**< The OutgoingWebRequest 'callback_data' */

src/misc.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3055,6 +3055,7 @@ OutgoingWebRequest *duplicate_outgoingwebrequest(OutgoingWebRequest *orig)
30553055
e->connect_timeout = orig->connect_timeout;
30563056
e->transfer_timeout = orig->transfer_timeout;
30573057
e->minimum_tls_version = orig->minimum_tls_version;
3058+
e->max_size = orig->max_size;
30583059
return e;
30593060
}
30603061

@@ -3098,7 +3099,7 @@ void download_file_async(const char *url,
30983099
url_start_async(request);
30993100
}
31003101

3101-
void url_callback(OutgoingWebRequest *r, const char *file, const char *memory, int memory_len, const char *errorbuf, int cached, void *ptr)
3102+
void url_callback(OutgoingWebRequest *r, const char *file, const char *memory, long long memory_len, const char *errorbuf, int cached, void *ptr)
31023103
{
31033104
OutgoingWebResponse *response;
31043105

src/url_curl.c

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ struct Download
3838
FILE *file_fd; /**< File open for writing (otherwise NULL) */
3939
char *filename;
4040
char *memory_data; /**< Memory for writing response (otherwise NULL) */
41-
int memory_data_len; /**< Size of memory_data */
42-
int memory_data_allocated; /**< Total allocated memory for 'memory_data' */
41+
long long memory_data_len; /**< Size of memory_data */
42+
long long memory_data_allocated; /**< Total allocated memory for 'memory_data' */
43+
int cap_exceeded; /**< Set to 1 by do_download_memory when max_size was hit */
4344
};
4445

4546
CURLM *multihandle = NULL;
@@ -173,12 +174,21 @@ static size_t do_download_memory(void *ptr, size_t size, size_t nmemb, void *str
173174
{
174175
// DUPLICATE CODE: same as src/url_unreal.c, well.. sortof
175176
Download *handle = (Download *)stream;
176-
int write_sz = size * nmemb;
177-
int size_required = handle->memory_data_len + write_sz;
177+
size_t write_sz = size * nmemb;
178+
long long size_required = handle->memory_data_len + (long long)write_sz;
179+
180+
if (size_required > handle->request->max_size)
181+
{
182+
handle->cap_exceeded = 1;
183+
safe_free(handle->memory_data);
184+
handle->memory_data_len = 0;
185+
handle->memory_data_allocated = 0;
186+
return 0; /* aborts the curl transfer; real error surfaced in url_check_multi_handles */
187+
}
178188

179189
if (size_required >= handle->memory_data_allocated - 1) // the -1 is for zero termination, even though it is binary..
180190
{
181-
int newsize = ((size_required / URL_MEMORY_BACKED_CHUNK_SIZE)+1)*URL_MEMORY_BACKED_CHUNK_SIZE;
191+
long long newsize = ((size_required / URL_MEMORY_BACKED_CHUNK_SIZE)+1)*URL_MEMORY_BACKED_CHUNK_SIZE;
182192
char *newptr = realloc(handle->memory_data, newsize);
183193
if (!newptr)
184194
{
@@ -248,7 +258,16 @@ static void url_check_multi_handles(void)
248258
}
249259
else
250260
{
251-
url_callback(handle->request, NULL, NULL, 0, handle->errorbuf, 0, handle->request->callback_data);
261+
char capbuf[128];
262+
const char *err = handle->errorbuf;
263+
if (handle->cap_exceeded)
264+
{
265+
snprintf(capbuf, sizeof(capbuf),
266+
"Response too large (maximum: %lld bytes)",
267+
handle->request->max_size);
268+
err = capbuf;
269+
}
270+
url_callback(handle->request, NULL, NULL, 0, err, 0, handle->request->callback_data);
252271
}
253272

254273
if (handle->filename && !handle->request->keep_file)
@@ -349,6 +368,10 @@ void url_start_async(OutgoingWebRequest *request)
349368
if (!request->url || !request->http_method)
350369
abort();
351370

371+
/* Set request defaults */
372+
if (request->max_size <= 0)
373+
request->max_size = DOWNLOAD_MAX_SIZE;
374+
352375
curl = curl_easy_init();
353376
if (!curl)
354377
{

src/url_unreal.c

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ struct Download
5151
FILE *file_fd; /**< File open for writing (otherwise NULL) */
5252
char *filename;
5353
char *memory_data; /**< Memory for writing response (otherwise NULL) */
54-
int memory_data_len; /**< Size of memory_data */
55-
int memory_data_allocated; /**< Total allocated memory for 'memory_data' */
54+
long long memory_data_len; /**< Size of memory_data */
55+
long long memory_data_allocated; /**< Total allocated memory for 'memory_data' */
5656
char errorbuf[512];
5757
char *hostname; /**< Parsed hostname (from 'url') */
5858
int port; /**< Parsed port (from 'url') */
@@ -73,7 +73,7 @@ struct Download
7373
time_t download_started;
7474
int dns_refcnt;
7575
TransferEncoding transfer_encoding;
76-
long chunk_remaining;
76+
long long chunk_remaining;
7777
char *redirect_new_location;
7878
};
7979

@@ -179,6 +179,8 @@ void url_start_async(OutgoingWebRequest *request)
179179
request->connect_timeout = DOWNLOAD_CONNECT_TIMEOUT;
180180
if (request->transfer_timeout == 0)
181181
request->transfer_timeout = DOWNLOAD_TRANSFER_TIMEOUT;
182+
if (request->max_size <= 0)
183+
request->max_size = DOWNLOAD_MAX_SIZE;
182184

183185
handle = safe_alloc(sizeof(Download));
184186
handle->download_started = TStime();
@@ -813,17 +815,17 @@ int https_handle_response_header(Download *handle, char *readbuf, int n)
813815
return 1;
814816
}
815817

816-
int https_handle_response_body_memory(Download *handle, const char *ptr, int write_sz)
818+
long long https_handle_response_body_memory(Download *handle, const char *ptr, long long write_sz)
817819
{
818820
// DUPLICATE CODE: same as src/url_curl.c, well... sortof
819-
int size_required = handle->memory_data_len + write_sz;
821+
long long size_required = handle->memory_data_len + write_sz;
820822

821823
if (handle->memory_data == NULL)
822824
return 0; /* Normally does not happen as it is preallocated, but could happen upon unwinding cancels.. */
823825

824826
if (size_required >= handle->memory_data_allocated - 1) // the -1 is for zero termination, even though it is binary..
825827
{
826-
int newsize = ((size_required / URL_MEMORY_BACKED_CHUNK_SIZE)+1)*URL_MEMORY_BACKED_CHUNK_SIZE;
828+
long long newsize = ((size_required / URL_MEMORY_BACKED_CHUNK_SIZE)+1)*URL_MEMORY_BACKED_CHUNK_SIZE;
827829
char *newptr = realloc(handle->memory_data, newsize);
828830
if (!newptr)
829831
{
@@ -858,7 +860,14 @@ int https_handle_response_body(Download *handle, char *readbuf, int pktsize)
858860
{
859861
/* Ohh.. so easy! */
860862
if (handle->request->store_in_file == 0)
863+
{
864+
if (handle->memory_data_len + pktsize > handle->request->max_size)
865+
{
866+
https_cancel(handle, "Response too large (maximum: %lld bytes)", handle->request->max_size);
867+
return 0; /* handle freed */
868+
}
861869
https_handle_response_body_memory(handle, readbuf, pktsize);
870+
}
862871
else if (handle->file_fd)
863872
fwrite(readbuf, 1, pktsize, handle->file_fd);
864873
return 1;
@@ -886,9 +895,17 @@ int https_handle_response_body(Download *handle, char *readbuf, int pktsize)
886895
if (handle->chunk_remaining > 0)
887896
{
888897
/* Eat it */
889-
int eat = MIN(handle->chunk_remaining, n);
898+
long long eat = MIN(handle->chunk_remaining, n);
890899
if (handle->request->store_in_file == 0)
900+
{
901+
if (handle->memory_data_len + eat > handle->request->max_size)
902+
{
903+
https_cancel(handle, "Response too large (maximum: %lld bytes)", handle->request->max_size);
904+
safe_free(free_this_buffer);
905+
return 0; /* handle freed */
906+
}
891907
https_handle_response_body_memory(handle, buf, eat);
908+
}
892909
else if (handle->file_fd)
893910
fwrite(buf, 1, eat, handle->file_fd);
894911
n -= eat;
@@ -943,10 +960,10 @@ int https_handle_response_body(Download *handle, char *readbuf, int pktsize)
943960
}
944961
buf[i] = '\0'; /* cut at LF */
945962
i++; /* point to next data */
946-
handle->chunk_remaining = strtol(buf, NULL, 16);
963+
handle->chunk_remaining = strtoll(buf, NULL, 16);
947964
if (handle->chunk_remaining < 0)
948965
{
949-
https_cancel(handle, "Negative chunk encountered (%ld)", handle->chunk_remaining);
966+
https_cancel(handle, "Negative chunk encountered (%lld)", handle->chunk_remaining);
950967
safe_free(free_this_buffer);
951968
return 0;
952969
}

0 commit comments

Comments
 (0)