Buffer Overflow in WebSocket Handshake (lib/ws.c:1287)
What the report claims
Buffer overflow in curl's WebSocket handshake due to unsafe strcpy() call at lib/ws.c:1287
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
Key code references could not be verified in the targeted curl version.
Why this finding?
- The code at lib/ws.c:1282-1286 contains an explicit bounds check (randlen >= sizeof(keyval)) that returns CURLE_FAILED_INIT before strcpy is reached.
- The Sec-WebSocket-Key is generated locally by curl from 16 random bytes; its base64 length is 25 bytes, well within the 40-byte buffer.
- The attacker (malicious server) cannot influence the key length; the PoC merely logs the key curl sends.
- The report's own output shows curl completing the handshake successfully, confirming no crash or overflow occurs.
How this analysis ran 6 claims · 12 citations verified
Checked against curl master on 2025-10-21 (b4f57c804573). Test 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 detailed report. However, the code at lib/ws.c:1282-1286 shows a bounds check that guarantees randlen < sizeof(keyval) (40) before strcpy executes. The Sec-WebSocket-Key is generated by curl from 16 random bytes; its base64 encoding is 25 bytes, so the check always passes and no overflow occurs. The malicious server cannot control this value. The PoC output shows a successful handshake, not a crash. This is not a buffer overflow vulnerability.
Inspect all claims and source code 6 claims · 4 code excerpts
Report claims
The code uses strcpy(keyval, randstr) without bounds checking at lib/ws.c:1287
Read the reasoning
The report claims strcpy is used 'without proper bounds checking' but lines 1282-1286 show both a DEBUGASSERT and an explicit runtime check that randlen < sizeof(keyval) (40) before strcpy is called. The base64 encoding of 16 bytes produces 24 characters plus NUL = 25 bytes, well under 40.
The vulnerable function is Curl_ws_request() in lib/ws.c
Read the reasoning
The function name and location are correct, but the function is not vulnerable due to the bounds check.
A malicious WebSocket server can trigger the overflow by sending a Sec-WebSocket-Key header
Read the reasoning
The Sec-WebSocket-Key is generated by curl (the client), not sent by the server. The malicious server in the PoC only receives and logs the key; it cannot influence its length. The attacker does not control the input to strcpy.
The overflow can lead to memory corruption, potential remote code execution, and denial of service
Read the reasoning
Since the bounds check guarantees randlen < 40 and the actual encoded length is 25, no overflow occurs. Memory corruption, RCE, and DoS via this path are not possible.
The report provides a Python server and curl command that reproduce the vulnerability
Read the reasoning
The Python server only receives the handshake; it cannot cause an overflow. The curl command in the PoC completes normally (as shown in the report's own output), confirming no crash or corruption occurs.
The vulnerability has a CVSS score of 7.5 (HIGH) and is classified as CWE-120
Read the reasoning
CVSS 7.5 and CWE-120 are assigned based on a false premise. No buffer overflow exists, so the severity rating is inapplicable.
Deterministic checks that failed
- symbol strncpy() is not called in lib/ws.c at b4f57c804573; curl's own source checker (checksrc.pl) bans strncpy() at this revision
Read the original report (excerpt)
Source code
| 1243 | CURLcode Curl_ws_request(struct Curl_easy *data, struct dynbuf *req) |
| C2: CURLcode Curl_ws_request(struct Curl_easy *data, struct dynbuf *req) definition | |
| 1244 | { |
| 1245 | unsigned int i; |
| 1246 | CURLcode result = CURLE_OK; |
| 1247 | unsigned char rand[16]; |
| 1248 | char *randstr; |
| 1249 | size_t randlen; |
| 1250 | char keyval[40]; |
| 1251 | struct SingleRequest *k = &data->req; |
| 1252 | struct wsfield heads[]= { |
| 1253 | { |
| 1254 | /* The request MUST contain an |Upgrade| header field whose value |
| 1255 | MUST include the "websocket" keyword. */ |
| 1256 | "Upgrade", "websocket" |
| 1257 | }, |
| 1258 | { |
| 1259 | /* The request MUST include a header field with the name |
| 1260 | |Sec-WebSocket-Version|. The value of this header field MUST be |
| 1261 | 13. */ |
| 1262 | "Sec-WebSocket-Version", "13", |
| 1263 | }, |
| 1264 | { |
| 1265 | /* The request MUST include a header field with the name |
| 1266 | |Sec-WebSocket-Key|. The value of this header field MUST be a nonce |
| 1267 | consisting of a randomly selected 16-byte value that has been |
| 1268 | base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be |
| 1269 | selected randomly for each connection. */ |
| 1270 | "Sec-WebSocket-Key", NULL, |
| 1271 | } |
| 1272 | }; |
| 1273 | heads[2].val = &keyval[0]; |
| C3: heads[2].val = &keyval[0] - the keyval buffer holds the client-generated key | |
| 1274 | |
| 1275 | /* 16 bytes random */ |
| 1276 | result = Curl_rand(data, (unsigned char *)rand, sizeof(rand)); |
| C3: Curl_rand generates 16 random bytes locally | |
| C5: Random bytes generated by curl, not attacker | |
| 1277 | if(result) |
| 1278 | return result; |
| 1279 | result = curlx_base64_encode((char *)rand, sizeof(rand), &randstr, &randlen); |
| C3: curlx_base64_encode encodes the locally-generated random bytes | |
| 1280 | if(result) |
| 1281 | return result; |
| 1282 | DEBUGASSERT(randlen < sizeof(keyval)); |
| C1: DEBUGASSERT(randlen < sizeof(keyval)) asserts the bound | |
| 1283 | if(randlen >= sizeof(keyval)) { |
| C1: Explicit runtime check: if(randlen >= sizeof(keyval)) { free(randstr); return CURLE_FAILED_INIT; } | |
| C4: Bounds check prevents overflow: if(randlen >= sizeof(keyval)) return CURLE_FAILED_INIT | |
| C5: Bounds check blocks any oversized input | |
| C6: Bounds check prevents the claimed overflow | |
| 1284 | free(randstr); |
| 1285 | return CURLE_FAILED_INIT; |
| 1286 | } |
| 1287 | strcpy(keyval, randstr); |
| C1: strcpy(keyval, randstr) only executes after the bounds check passes | |
| 1288 | free(randstr); |
| 1289 | for(i = 0; !result && (i < CURL_ARRAYSIZE(heads)); i++) { |
| 1290 | if(!Curl_checkheaders(data, heads[i].name, strlen(heads[i].name))) { |
| 1291 | result = curlx_dyn_addf(req, "%s: %s\r\n", heads[i].name, |
| 1292 | heads[i].val); |
| 1293 | } |
| 1294 | } |
| 1295 | data->state.http_hd_upgrade = TRUE; |
| 1296 | k->upgr101 = UPGR101_WS; |
| 1297 | data->conn->bits.upgrade_in_progress = TRUE; |
| 1298 | return result; |
| 1299 | } |
| 1300 | |
| 1301 | static void ws_conn_dtor(void *key, size_t klen, void *entry) |
| 1302 | { |
| 1303 | struct websocket *ws = entry; |
| 1304 | (void)key; |
| 1305 | (void)klen; |
| 1306 | Curl_bufq_free(&ws->recvbuf); |
| 1307 | Curl_bufq_free(&ws->sendbuf); |
| 1308 | free(ws); |
| 1309 | } |
| 1310 | |
| 1311 | /* |
| 1312 | * 'nread' is number of bytes of websocket data already in the buffer at |
| 248 | CURLcode curlx_base64_encode(const char *inputbuff, size_t insize, |
| C4: curlx_base64_encode output length for 16-byte input is 25 bytes (24 chars + NUL) | |
| 249 | char **outptr, size_t *outlen) |
| 250 | { |
| 251 | return base64_encode(Curl_base64encdec, '=', |
| 252 | inputbuff, insize, outptr, outlen); |
| 253 | } |
Show 56 lines (not cited by the judge)
| 1217 | CURLcode Curl_ws_request(struct Curl_easy *data, struct dynbuf *req) |
| 1218 | { |
| 1219 | unsigned int i; |
| 1220 | CURLcode result = CURLE_OK; |
| 1221 | unsigned char rand[16]; |
| 1222 | char *randstr; |
| 1223 | size_t randlen; |
| 1224 | char keyval[40]; |
| 1225 | struct SingleRequest *k = &data->req; |
| 1226 | struct wsfield heads[]= { |
| 1227 | { |
| 1228 | /* The request MUST contain an |Upgrade| header field whose value |
| 1229 | MUST include the "websocket" keyword. */ |
| 1230 | "Upgrade", "websocket" |
| 1231 | }, |
| 1232 | { |
| 1233 | /* The request MUST include a header field with the name |
| 1234 | |Sec-WebSocket-Version|. The value of this header field MUST be |
| 1235 | 13. */ |
| 1236 | "Sec-WebSocket-Version", "13", |
| 1237 | }, |
| 1238 | { |
| 1239 | /* The request MUST include a header field with the name |
| 1240 | |Sec-WebSocket-Key|. The value of this header field MUST be a nonce |
| 1241 | consisting of a randomly selected 16-byte value that has been |
| 1242 | base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be |
| 1243 | selected randomly for each connection. */ |
| 1244 | "Sec-WebSocket-Key", NULL, |
| 1245 | } |
| 1246 | }; |
| 1247 | heads[2].val = &keyval[0]; |
| 1248 | |
| 1249 | /* 16 bytes random */ |
| 1250 | result = Curl_rand(data, (unsigned char *)rand, sizeof(rand)); |
| 1251 | if(result) |
| 1252 | return result; |
| 1253 | result = curlx_base64_encode((char *)rand, sizeof(rand), &randstr, &randlen); |
| 1254 | if(result) |
| 1255 | return result; |
| 1256 | DEBUGASSERT(randlen < sizeof(keyval)); |
| 1257 | if(randlen >= sizeof(keyval)) { |
| 1258 | free(randstr); |
| 1259 | return CURLE_FAILED_INIT; |
| 1260 | } |
| 1261 | strcpy(keyval, randstr); |
| 1262 | free(randstr); |
| 1263 | for(i = 0; !result && (i < CURL_ARRAYSIZE(heads)); i++) { |
| 1264 | if(!Curl_checkheaders(data, heads[i].name, strlen(heads[i].name))) { |
| 1265 | result = curlx_dyn_addf(req, "%s: %s\r\n", heads[i].name, |
| 1266 | heads[i].val); |
| 1267 | } |
| 1268 | } |
| 1269 | data->state.http_hd_upgrade = TRUE; |
| 1270 | k->upgr101 = UPGR101_WS; |
| 1271 | return result; |
| 1272 | } |
Show 1 lines (not cited by the judge)
| 293 | #define DEBUGASSERT(x) assert(x) |