The report missed a size check.
Buffer Overflow Risk in Curl_inet_ntop and inet_ntop4
What the report claims
The report claims an unchecked copy can overflow a buffer.
Compare with curl's recorded outcome
Labeled AI slop
curl's maintainer lists this report as AI slop
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The real function checks the size and returns before the copy if the buffer is too small.
curl policy: API misuseWhy this finding?
- The central claim (C2) that inet_ntop4 uses strcpy without ensuring the destination buffer is large enough is directly contradicted by the code at lib/inet_ntop.c:76, which checks 'if(len == 0 || len >= size)' before the strcpy at line 80.
- The reporter's proof-of-concept code is not the actual curl implementation; it omits the critical bounds check that exists in the real code.
- Both inet_ntop4 and inet_ntop6 perform runtime buffer size validation before copying to the destination buffer, making the claimed buffer overflow not reachable when the API is used as documented.
- The project policy lists 'API misuse' as not a security issue: if an application passes a buffer smaller than documented requirements, that is misuse, not a curl vulnerability.
The guard before the copy
If the buffer is too small, the function returns. The copy comes after that check.
Inspect lib/inet_ntop.c:76 →| 75 | len = strlen(tmp); |
| 76 | if(len == 0 || len >= size) { |
| 77 | errno = ENOSPC; |
| 78 | return (NULL); |
| 79 | } |
| 80 | strcpy(dst, tmp); |
| 81 | return dst; |
How this analysis ran 6 claims · 9 citations verified
Checked against curl master on 2024-12-08 (59fec5ac43ad). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the report. The actual curl code in lib/inet_ntop.c includes a bounds check at line 76 (`if(len == 0 || len >= size)`) before the strcpy at line 80, which prevents the buffer overflow you describe. Your proof-of-concept omits this check. Both inet_ntop4 and inet_ntop6 validate the destination buffer size at runtime. When the API is used as documented (caller provides a buffer of at least 16 bytes for IPv4, 46 for IPv6), no overflow occurs. Per curl's policy, API misuse (passing undersized buffers) is not considered a security vulnerability in curl.
Inspect all claims and source code 6 claims · 4 code excerpts
Report claims
Curl_inet_ntop lacks buffer size validation before delegating to inet_ntop4 or inet_ntop6
Read the reasoning
Curl_inet_ntop does not perform buffer size validation before delegating; it passes the size parameter through to the helper functions which do validate.
inet_ntop4 uses strcpy without ensuring the destination buffer is large enough
Read the reasoning
The actual curl code at line 76 explicitly validates that the formatted string length is less than the destination buffer size before calling strcpy. The reporter's POC omits this check.
Minimum buffer size of 16 bytes is required for IPv4 addresses like "255.255.255.255\0"
Read the reasoning
16 bytes is correct for the maximum IPv4 string representation including null terminator.
Minimum buffer size of 46 bytes is required for IPv6 addresses like "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\0"
Read the reasoning
46 bytes is the correct minimum for full IPv6 address representation.
Assertions (DEBUGASSERT) in inet_ntop4 are disabled in production builds, removing safety checks
Read the reasoning
DEBUGASSERT is indeed disabled in production builds, but a runtime check at line 76 remains active.
snprintf is used to format IPv4 addresses but does not guarantee safety if the buffer is too small
Read the reasoning
msnprintf writes to a sufficiently large temporary buffer; the destination buffer size is validated at line 76 before any copy occurs.
Read the original report (excerpt)
Source code
| 61 | static char *inet_ntop4(const unsigned char *src, char *dst, size_t size) |
| 62 | { |
| 63 | char tmp[sizeof("255.255.255.255")]; |
| C3: tmp buffer sized as sizeof("255.255.255.255") = 16 bytes including null terminator | |
| 64 | size_t len; |
| 65 | |
| 66 | DEBUGASSERT(size >= 16); |
| C3: DEBUGASSERT(size >= 16) documents the minimum | |
| C5: DEBUGASSERT(size >= 16) in inet_ntop4 | |
| 67 | |
| 68 | tmp[0] = '\0'; |
| 69 | (void)msnprintf(tmp, sizeof(tmp), "%d.%d.%d.%d", |
| C6: msnprintf writes to tmp buffer sized for maximum IPv4 string (16 bytes), not directly to destination | |
| 70 | ((int)((unsigned char)src[0])) & 0xff, |
| 71 | ((int)((unsigned char)src[1])) & 0xff, |
| 72 | ((int)((unsigned char)src[2])) & 0xff, |
| 73 | ((int)((unsigned char)src[3])) & 0xff); |
| 74 | |
| 75 | len = strlen(tmp); |
| 76 | if(len == 0 || len >= size) { |
| C2: inet_ntop4 checks 'if(len == 0 || len >= size)' before strcpy at line 80, ensuring destination buffer is large enough | |
| C6: Runtime check 'len >= size' validates destination buffer before copy | |
| 77 | errno = ENOSPC; |
| 78 | return (NULL); |
| 79 | } |
| 80 | strcpy(dst, tmp); |
| 81 | return dst; |
| 82 | } |
| 192 | char *Curl_inet_ntop(int af, const void *src, char *buf, size_t size) |
| C1: Curl_inet_ntop at line 192-202 delegates directly to inet_ntop4/inet_ntop6 without its own size validation | |
| 193 | { |
| 194 | switch(af) { |
| 195 | case AF_INET: |
| 196 | return inet_ntop4((const unsigned char *)src, buf, size); |
| 197 | case AF_INET6: |
| 198 | return inet_ntop6((const unsigned char *)src, buf, size); |
| 199 | default: |
| 200 | errno = EAFNOSUPPORT; |
| 201 | return NULL; |
| 202 | } |
| 203 | } |
| 294 | #define DEBUGASSERT(x) assert(x) |
| C5: DEBUGASSERT defined as assert(x), which is disabled when NDEBUG is defined (production builds) |
| 87 | static char *inet_ntop6(const unsigned char *src, char *dst, size_t size) |
| 88 | { |
| 89 | /* |
| 90 | * Note that int32_t and int16_t need only be "at least" large enough |
| 91 | * to contain a value of the specified size. On some systems, like |
| 92 | * Crays, there is no such thing as an integer variable with 16 bits. |
| 93 | * Keep this in mind if you think this function should have been coded |
| 94 | * to use pointer overlays. All the world's not a VAX. |
| 95 | */ |
| 96 | char tmp[sizeof("ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255")]; |
| C4: tmp buffer in inet_ntop6 sized for maximum IPv6 representation including embedded IPv4 | |
| 97 | char *tp; |
| 98 | struct { |
| 99 | int base; |
| 100 | int len; |
| 101 | } best, cur; |
| 102 | unsigned int words[IN6ADDRSZ / INT16SZ]; |
| 103 | int i; |
| 104 | |
| 105 | /* Preprocess: |
| 106 | * Copy the input (bytewise) array into a wordwise array. |
| 107 | * Find the longest run of 0x00's in src[] for :: shorthanding. |
| 108 | */ |
| 109 | memset(words, '\0', sizeof(words)); |
| 110 | for(i = 0; i < IN6ADDRSZ; i++) |
| 111 | words[i/2] |= ((unsigned int)src[i] << ((1 - (i % 2)) << 3)); |
| 112 | |
| 113 | best.base = -1; |
| 114 | cur.base = -1; |
| 115 | best.len = 0; |
| 116 | cur.len = 0; |
| 117 | |
| 118 | for(i = 0; i < (IN6ADDRSZ / INT16SZ); i++) { |
| 119 | if(words[i] == 0) { |
| 120 | if(cur.base == -1) { |
| 121 | cur.base = i; cur.len = 1; |
| 122 | } |
| 123 | else |
| 124 | cur.len++; |
| 125 | } |
| 126 | else if(cur.base != -1) { |
| 127 | if(best.base == -1 || cur.len > best.len) |
| 128 | best = cur; |
| 129 | cur.base = -1; |
| 130 | } |
| 131 | } |
| 132 | if((cur.base != -1) && (best.base == -1 || cur.len > best.len)) |
| 133 | best = cur; |
| 134 | if(best.base != -1 && best.len < 2) |
| 135 | best.base = -1; |
| 136 | /* Format the result. */ |
| 137 | tp = tmp; |
| 138 | for(i = 0; i < (IN6ADDRSZ / INT16SZ); i++) { |
| 139 | /* Are we inside the best run of 0x00's? */ |
| 140 | if(best.base != -1 && i >= best.base && i < (best.base + best.len)) { |
| 141 | if(i == best.base) |
| 142 | *tp++ = ':'; |
| 143 | continue; |
| 144 | } |
| 145 | |
| 146 | /* Are we following an initial run of 0x00s or any real hex? |
| 147 | */ |
| 148 | if(i) |
| 149 | *tp++ = ':'; |
| 150 | |
| 151 | /* Is this address an encapsulated IPv4? |
| 152 | */ |
| 153 | if(i == 6 && best.base == 0 && |
| 154 | (best.len == 6 || (best.len == 5 && words[5] == 0xffff))) { |
| 155 | if(!inet_ntop4(src + 12, tp, sizeof(tmp) - (tp - tmp))) { |
| 156 | errno = ENOSPC; |
| 157 | return (NULL); |
| 158 | } |
| 159 | tp += strlen(tp); |
| 160 | break; |
| 161 | } |
| 162 | tp += msnprintf(tp, 5, "%x", words[i]); |
| 163 | } |
| 164 | |
| 165 | /* Was it a trailing run of 0x00's? |
| 166 | */ |
| 167 | if(best.base != -1 && (best.base + best.len) == (IN6ADDRSZ / INT16SZ)) |
| 168 | *tp++ = ':'; |
| 169 | *tp++ = '\0'; |
| 170 | |
| 171 | /* Check for overflow, copy, and we are done. |
| 172 | */ |
| 173 | if((size_t)(tp - tmp) > size) { |
| 174 | errno = ENOSPC; |
| 175 | return (NULL); |
| 176 | } |
| 177 | strcpy(dst, tmp); |
| 178 | return dst; |
| 179 | } |