Skip to content

Commit 0425514

Browse files
committed
fix: properly handle IPv6 addresses in HTTP Host headers
This commit fixes IPv6 address handling in HTTP client Host headers by adding bracket notation when required and improving URL parsing validation. Changes: - Add automatic bracket wrapping for unbracketed IPv6 addresses in Host headers for both standard and non-standard ports - Add IPv6 bracketing for HTTPS default port (443) to ensure RFC compliance even when port is omitted (e.g., Host: [::1]) - Fix off-by-one error in IPv6 bracket stripping (was removing one extra character) - Fix incorrect length calculation in flb_utils_copy_host_sds for bracketed IPv6 extraction (changed from absolute position to relative length to properly account for pos_init offset) - Strip IPv6 zone IDs (e.g., %eth0) from Host headers per RFC 3986 which prohibits zone IDs in URIs (e.g., fe80::1%eth0 becomes [fe80::1]:8080 in Host header) - Perform zone ID stripping before inet_pton() validation to ensure proper IPv6 address detection for link-local addresses - Add URI path prepending for URLs with query/fragment but no path (e.g., http://example.com?query=1 becomes /?query=1) per RFC 7230 - Constrain IPv6 bracket validation to host portion only, preventing false negatives when brackets appear in URL paths or query strings - Update validate_ipv6_brackets() to recognize '?' and '#' as host delimiters in addition to '/' - Refactor URL parsing logic to eliminate duplication - Use memchr with length limit for consistent and safe bracket detection in both IPv6 and non-IPv6 cases - Improve error handling in URL parsing with proper cleanup on failure - Update TLS flag checking to use flb_stream_get_flag_status() for more reliable detection Tests: - Add test for IPv6 with HTTPS on default port 443 - Add test cases for IPv6 addresses with zone IDs (verifying zone ID stripping behavior) - Add test cases for brackets in URL paths and query strings - Add test cases for malformed bracket scenarios Signed-off-by: Shelby Hagman <[email protected]>
1 parent 4f8c50b commit 0425514

File tree

5 files changed

+456
-31
lines changed

5 files changed

+456
-31
lines changed

src/flb_http_client.c

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@
3333
#define _GNU_SOURCE
3434
#include <string.h>
3535

36+
#ifdef FLB_SYSTEM_WINDOWS
37+
#include <winsock2.h>
38+
#include <ws2tcpip.h>
39+
#endif
40+
3641
#include <fluent-bit/flb_info.h>
42+
#include <fluent-bit/flb_compat.h>
3743
#include <fluent-bit/flb_kv.h>
3844
#include <fluent-bit/flb_log.h>
3945
#include <fluent-bit/flb_mem.h>
@@ -617,11 +623,55 @@ static int add_host_and_content_length(struct flb_http_client *c)
617623
out_port = c->port;
618624
}
619625

620-
if (c->flags & FLB_IO_TLS && out_port == 443) {
621-
tmp = flb_sds_copy(host, out_host, strlen(out_host));
626+
/* Check if out_host is an unbracketed IPv6 address */
627+
struct in6_addr addr;
628+
char *zone_id;
629+
char addr_buf[INET6_ADDRSTRLEN];
630+
int is_ipv6 = 0;
631+
int is_https_default_port;
632+
const char *host_for_header;
633+
634+
if (out_host && out_host[0] != '[') {
635+
/* Strip zone ID if present (e.g., fe80::1%eth0 -> fe80::1) */
636+
zone_id = strchr(out_host, '%');
637+
if (zone_id) {
638+
len = zone_id - out_host;
639+
if (len < INET6_ADDRSTRLEN) {
640+
memcpy(addr_buf, out_host, len);
641+
addr_buf[len] = '\0';
642+
is_ipv6 = (inet_pton(AF_INET6, addr_buf, &addr) == 1);
643+
}
644+
}
645+
else {
646+
is_ipv6 = (inet_pton(AF_INET6, out_host, &addr) == 1);
647+
}
648+
}
649+
650+
/* Use stripped address (without zone ID) for Host header if zone ID was present */
651+
host_for_header = (is_ipv6 && zone_id) ? addr_buf : out_host;
652+
653+
/* Check if connection uses TLS and port is 443 (HTTPS default) */
654+
is_https_default_port = flb_stream_get_flag_status(&u->base, FLB_IO_TLS) && out_port == 443;
655+
656+
if (is_https_default_port) {
657+
if (is_ipv6) {
658+
/* IPv6 address needs brackets for RFC compliance */
659+
tmp = flb_sds_printf(&host, "[%s]", host_for_header);
660+
}
661+
else {
662+
/* HTTPS on default port 443 - omit port from Host header */
663+
tmp = flb_sds_copy(host, out_host, strlen(out_host));
664+
}
622665
}
623666
else {
624-
tmp = flb_sds_printf(&host, "%s:%i", out_host, out_port);
667+
if (is_ipv6) {
668+
/* IPv6 address needs brackets when combined with port */
669+
tmp = flb_sds_printf(&host, "[%s]:%i", host_for_header, out_port);
670+
}
671+
else {
672+
/* IPv4 address, domain name, or already bracketed IPv6 */
673+
tmp = flb_sds_printf(&host, "%s:%i", out_host, out_port);
674+
}
625675
}
626676

627677
if (!tmp) {

src/flb_network.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#ifdef FLB_SYSTEM_WINDOWS
3131
#define poll WSAPoll
3232
#include <winsock2.h>
33+
#include <ws2tcpip.h>
3334
#else
3435
#include <sys/poll.h>
3536
#endif

src/flb_utils.c

Lines changed: 179 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,13 +1431,103 @@ static char *flb_utils_copy_host_sds(const char *string, int pos_init, int pos_e
14311431
if (string[pos_end-1] != ']') {
14321432
return NULL;
14331433
}
1434-
return flb_sds_create_len(string + pos_init + 1, pos_end - 1);
1434+
return flb_sds_create_len(string + pos_init + 1, pos_end - pos_init - 2);
14351435
}
14361436
else {
1437-
return flb_sds_create_len(string + pos_init, pos_end);
1437+
return flb_sds_create_len(string + pos_init, pos_end - pos_init);
14381438
}
14391439
}
14401440

1441+
/* Validate IPv6 bracket syntax in URL host part */
1442+
static int validate_ipv6_brackets(const char *p, const char **out_bracket)
1443+
{
1444+
const char *host_end;
1445+
const char *bracket = NULL;
1446+
const char *closing;
1447+
const char *query_or_fragment;
1448+
1449+
/* Only inspect the host portion (up to the first '/', '?', or '#') */
1450+
host_end = strchr(p, '/');
1451+
query_or_fragment = strpbrk(p, "?#");
1452+
1453+
/* Use the earliest delimiter found */
1454+
if (query_or_fragment && (!host_end || query_or_fragment < host_end)) {
1455+
host_end = query_or_fragment;
1456+
}
1457+
1458+
if (!host_end) {
1459+
host_end = p + strlen(p);
1460+
}
1461+
1462+
if (p[0] == '[') {
1463+
closing = memchr(p, ']', host_end - p);
1464+
if (!closing || closing == p + 1) {
1465+
/* Missing closing bracket or empty brackets [] */
1466+
return -1;
1467+
}
1468+
bracket = closing;
1469+
}
1470+
else {
1471+
/* Non-bracketed hosts must not contain ']' before the first '/' */
1472+
closing = memchr(p, ']', host_end - p);
1473+
if (closing) {
1474+
return -1;
1475+
}
1476+
}
1477+
1478+
if (out_bracket) {
1479+
*out_bracket = bracket;
1480+
}
1481+
return 0;
1482+
}
1483+
1484+
/* Helper to create URI with prepended '/' if it starts with '?' or '#' */
1485+
static char *create_uri_with_slash(const char *uri_part)
1486+
{
1487+
char *uri;
1488+
size_t uri_part_len;
1489+
1490+
if (!uri_part || *uri_part == '\0') {
1491+
return flb_strdup("/");
1492+
}
1493+
1494+
/* If URI starts with '?' or '#', prepend '/' */
1495+
if (*uri_part == '?' || *uri_part == '#') {
1496+
uri_part_len = strlen(uri_part);
1497+
/* Allocate space for '/' + uri_part + '\0' */
1498+
uri = flb_malloc(uri_part_len + 2);
1499+
if (!uri) {
1500+
return NULL;
1501+
}
1502+
uri[0] = '/';
1503+
/* +1 to include '\0' */
1504+
memcpy(uri + 1, uri_part, uri_part_len + 1);
1505+
return uri;
1506+
}
1507+
1508+
/* URI already starts with '/' or is a normal path */
1509+
return flb_strdup(uri_part);
1510+
}
1511+
1512+
/* SDS version: Helper to create URI with prepended '/' if it starts with '?' or '#' */
1513+
static flb_sds_t create_uri_with_slash_sds(const char *uri_part)
1514+
{
1515+
char *result;
1516+
flb_sds_t uri;
1517+
1518+
/* Use the regular version to create the string */
1519+
result = create_uri_with_slash(uri_part);
1520+
if (!result) {
1521+
return NULL;
1522+
}
1523+
1524+
/* Convert to SDS */
1525+
uri = flb_sds_create(result);
1526+
flb_free(result);
1527+
1528+
return uri;
1529+
}
1530+
14411531
int flb_utils_url_split(const char *in_url, char **out_protocol,
14421532
char **out_host, char **out_port, char **out_uri)
14431533
{
@@ -1448,6 +1538,7 @@ int flb_utils_url_split(const char *in_url, char **out_protocol,
14481538
char *p;
14491539
char *tmp;
14501540
char *sep;
1541+
const char *bracket = NULL;
14511542

14521543
/* Protocol */
14531544
p = strstr(in_url, "://");
@@ -1467,9 +1558,17 @@ int flb_utils_url_split(const char *in_url, char **out_protocol,
14671558
/* Advance position after protocol */
14681559
p += 3;
14691560

1470-
/* Check for first '/' */
1561+
/* Validate IPv6 brackets */
14711562
sep = strchr(p, '/');
1472-
tmp = strchr(p, ':');
1563+
if (validate_ipv6_brackets(p, &bracket) < 0) {
1564+
flb_errno();
1565+
goto error;
1566+
}
1567+
if (bracket) {
1568+
tmp = strchr(bracket, ':');
1569+
} else {
1570+
tmp = strchr(p, ':');
1571+
}
14731572

14741573
/* Validate port separator is found before the first slash */
14751574
if (sep && tmp) {
@@ -1478,36 +1577,53 @@ int flb_utils_url_split(const char *in_url, char **out_protocol,
14781577
}
14791578
}
14801579

1580+
/* Extract host if port separator was found */
14811581
if (tmp) {
14821582
host = flb_copy_host(p, 0, tmp - p);
14831583
if (!host) {
14841584
flb_errno();
14851585
goto error;
14861586
}
14871587
p = tmp + 1;
1588+
}
14881589

1489-
/* Look for an optional URI */
1490-
tmp = strchr(p, '/');
1590+
/* Find URI delimiter (/, ?, or #) */
1591+
tmp = strpbrk(p, "/?#");
1592+
1593+
if (!host) {
1594+
/* No port: extract host */
14911595
if (tmp) {
1492-
port = mk_string_copy_substr(p, 0, tmp - p);
1493-
uri = flb_strdup(tmp);
1596+
host = flb_copy_host(p, 0, tmp - p);
14941597
}
14951598
else {
1496-
port = flb_strdup(p);
1497-
uri = flb_strdup("/");
1599+
host = flb_copy_host(p, 0, strlen(p));
1600+
}
1601+
if (!host) {
1602+
flb_errno();
1603+
goto error;
14981604
}
14991605
}
15001606
else {
1501-
tmp = strchr(p, '/');
1607+
/* Port exists: extract port */
15021608
if (tmp) {
1503-
host = flb_copy_host(p, 0, tmp - p);
1504-
uri = flb_strdup(tmp);
1609+
port = mk_string_copy_substr(p, 0, tmp - p);
15051610
}
15061611
else {
1507-
host = flb_copy_host(p, 0, strlen(p));
1508-
uri = flb_strdup("/");
1612+
port = flb_strdup(p);
1613+
}
1614+
}
1615+
1616+
/* Extract URI */
1617+
if (tmp) {
1618+
uri = create_uri_with_slash(tmp);
1619+
if (!uri) {
1620+
flb_errno();
1621+
goto error;
15091622
}
15101623
}
1624+
else {
1625+
uri = flb_strdup("/");
1626+
}
15111627

15121628
if (!port) {
15131629
if (strcmp(protocol, "http") == 0) {
@@ -1529,6 +1645,15 @@ int flb_utils_url_split(const char *in_url, char **out_protocol,
15291645
if (protocol) {
15301646
flb_free(protocol);
15311647
}
1648+
if (host) {
1649+
flb_free(host);
1650+
}
1651+
if (port) {
1652+
flb_free(port);
1653+
}
1654+
if (uri) {
1655+
flb_free(uri);
1656+
}
15321657

15331658
return -1;
15341659
}
@@ -1544,6 +1669,7 @@ int flb_utils_url_split_sds(const flb_sds_t in_url, flb_sds_t *out_protocol,
15441669
char *p = NULL;
15451670
char *tmp = NULL;
15461671
char *sep = NULL;
1672+
const char *bracket = NULL;
15471673

15481674
/* Protocol */
15491675
p = strstr(in_url, "://");
@@ -1563,9 +1689,17 @@ int flb_utils_url_split_sds(const flb_sds_t in_url, flb_sds_t *out_protocol,
15631689
/* Advance position after protocol */
15641690
p += 3;
15651691

1566-
/* Check for first '/' */
1692+
/* Validate IPv6 brackets */
15671693
sep = strchr(p, '/');
1568-
tmp = strchr(p, ':');
1694+
if (validate_ipv6_brackets(p, &bracket) < 0) {
1695+
flb_errno();
1696+
goto error;
1697+
}
1698+
if (bracket) {
1699+
tmp = strchr(bracket, ':');
1700+
} else {
1701+
tmp = strchr(p, ':');
1702+
}
15691703

15701704
/* Validate port separator is found before the first slash */
15711705
if (sep && tmp) {
@@ -1574,37 +1708,54 @@ int flb_utils_url_split_sds(const flb_sds_t in_url, flb_sds_t *out_protocol,
15741708
}
15751709
}
15761710

1711+
/* Extract host if port separator was found */
15771712
if (tmp) {
15781713
host = flb_utils_copy_host_sds(p, 0, tmp - p);
15791714
if (!host) {
15801715
flb_errno();
15811716
goto error;
15821717
}
15831718
p = tmp + 1;
1719+
}
15841720

1585-
/* Look for an optional URI */
1586-
tmp = strchr(p, '/');
1721+
/* Find URI delimiter (/, ?, or #) */
1722+
tmp = strpbrk(p, "/?#");
1723+
1724+
if (!host) {
1725+
/* No port: extract host */
15871726
if (tmp) {
1588-
port = flb_sds_create_len(p, tmp - p);
1589-
uri = flb_sds_create(tmp);
1727+
host = flb_utils_copy_host_sds(p, 0, tmp - p);
15901728
}
15911729
else {
1592-
port = flb_sds_create_len(p, strlen(p));
1593-
uri = flb_sds_create("/");
1730+
host = flb_utils_copy_host_sds(p, 0, strlen(p));
1731+
}
1732+
if (!host) {
1733+
flb_errno();
1734+
goto error;
15941735
}
15951736
}
15961737
else {
1597-
tmp = strchr(p, '/');
1738+
/* Port exists: extract port */
15981739
if (tmp) {
1599-
host = flb_utils_copy_host_sds(p, 0, tmp - p);
1600-
uri = flb_sds_create(tmp);
1740+
port = flb_sds_create_len(p, tmp - p);
16011741
}
16021742
else {
1603-
host = flb_utils_copy_host_sds(p, 0, strlen(p));
1604-
uri = flb_sds_create("/");
1743+
port = flb_sds_create_len(p, strlen(p));
16051744
}
16061745
}
16071746

1747+
/* Extract URI */
1748+
if (tmp) {
1749+
uri = create_uri_with_slash_sds(tmp);
1750+
if (!uri) {
1751+
flb_errno();
1752+
goto error;
1753+
}
1754+
}
1755+
else {
1756+
uri = flb_sds_create("/");
1757+
}
1758+
16081759
if (!port) {
16091760
if (strcmp(protocol, "http") == 0) {
16101761
port = flb_sds_create("80");

0 commit comments

Comments
 (0)