✓ProveIt
Examples / Report #3392174 Saved analysis

Buffer Overflow in WebSocket Handshake (lib/ws.c:1287)

curl · 2025-10-2113.8s analysis · $0.014 in model calls

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

References do not check out

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.

1 · Grounding
not grounded
no model · 8 references checked out, 1 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
6 claims · 2650 tokens · $0.0003
3 · Evidence
4 code snippets
real curl source at b4f57c804573
4 · Judgment
Nemotron-3-Ultra-550b-a55b
9011 tokens · 10.8s · $0.0135
5 · Receipts
12 of 12 citations verified
every citation must point at code the judge was shown
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

C1flaw contradicted

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.

C2location supported

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.

C3trigger contradicted

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.

C4impact contradicted

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.

C5reproduction contradicted

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.

C6impact contradicted

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)

Full report on HackerOne ↗

## Summary: Buffer overflow vulnerability in curl's WebSocket implementation due to unsafe use of strcpy() in the handshake process. The vulnerability is located at lib/ws.c:1287 where strcpy(keyval, randstr) is called without proper bounds checking, despite having a bounds check earlier in the code. **AI DISCLOSURE**: This vulnerability analysis was conducted with AI assistance. All technical details, code analysis, and exploit development were verified through manual testing and code review. The vulnerability exists in the actual curl source code and has been demonstrated with working proof of concept. ## Affected version curl version: curl 8.7.1 (x86_64-apple-darwin25.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.66.0 Release-Date: 2024-03-27 Platform: macOS 25.0.0 Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM SPNEGO SSL threadsafe UnixSockets ## Steps To Reproduce: 1. **Create malicious WebSocket server**: ```python # Save as websocket_exploit.py import socket class WebSocketExploitServer: def __init__(self, host='127.0.0.1', port=8080): self.host = host self.port = port def start_server(self): self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_socket.bind((self.host, self.port)) self.server_socket.listen(1) print(f"Malicious WebSocket server started on {self.host}:{self.port}") while True: client_socket, addr = self.server_socket.accept() print(f"Connection from {addr}") # Handle WebSocket handshake request = client_socket.recv(4096).decode('utf-8') # Parse Sec-WebSocket-Key to confirm vulnerability trigger for line in request.split('\n'): if line.startswith('Sec-WebSocket-Key:'): key = line.split(':', 1)[1].strip() print(f"Sec-WebSocket-Key: {key}") print(f"Key length: …

Source code

lib/ws.clines 1243–1312 · lines 1287-1287 cited by the report; definition of Curl_ws_request
1243CURLcode 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
1301static 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
lib/curlx/base64.clines 248–253 · definition of curlx_base64_encode
248CURLcode 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}
lib/ws.c@curl-8_16_0lines 1217–1272 · definition of Curl_ws_request as in release curl-8_16_0; differs from master; commits that changed it since: b3fc692568 lib: upgrade/multiplex handling
Show 56 lines (not cited by the judge)
1217CURLcode 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}
lib/curl_setup_once.hlines 293–293 · definition of DEBUGASSERT
Show 1 lines (not cited by the judge)
293#define DEBUGASSERT(x) assert(x)