✓ProveIt
Examples / Report #3650689 Saved analysis

CVE-2026-5773: wrong reuse of SMB connection

curl · 2026-04-0514.5s analysis · $0.021 in model calls

What the report claims

libcurl incorrectly reuses SMB connections across different shares on the same server, leading to data spoofing and access control bypass.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-5773 wrong reuse of SMB connection

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • url_match_proto_config has protocol-specific matching for SSH and FTP but completely lacks SMB share matching
  • Connection reuse logic in url_match_conn calls url_match_proto_config, allowing cross-share reuse
  • smb_do returns early when smbc->share is set, causing smb_request_state to use stale share with new path
  • This enables data spoofing across trust boundaries (shares) on the same server
  • The attack scenario is realistic: an application accessing multiple shares with same credentials can be tricked into reading attacker-controlled data from one share when requesting from another
How this analysis ran 5 claims · 10 citations verified

Checked against curl master on 2026-04-05 (acda4eae5eeb). 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
grounded
no model · 19 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3022 tokens · $0.0003
3 · Evidence
9 code snippets
real curl source at acda4eae5eeb · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
14466 tokens · 11.2s · $0.0206
5 · Receipts
10 of 11 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 and PoC. The code confirms the flaw: url_match_proto_config (lib/url.c:977-996) performs protocol-specific matching for SSH and FTP but has no SMB share check. This allows connection reuse across different shares. When reused, smb_do (lib/smb.c:1204) returns early because smbc->share is set from the first request, and smb_request_state then sends TREE_CONNECT for the old share while operating on the new path. This is a valid security issue enabling cross-share data spoofing. We will implement an smb_conns_match function and add the check to url_match_proto_config.

Inspect all claims and source code 5 claims · 9 code excerpts

Report claims

C1flaw supported

libcurl incorrectly reuses SMB connections across different shares on the same server, leading to data spoofing and access control bypass.

Read the reasoning

The code confirms that url_match_proto_config performs protocol-specific connection matching for SSH and FTP but has no equivalent check for SMB. This allows connections to be reused across different SMB shares on the same server.

C2location supported

The vulnerability is located in `lib/url.c` in the `url_match_proto_config` function, which does not verify if the new URL's SMB share matches the existing connection's `smbc->share`.

Read the reasoning

The vulnerability location is correctly identified. url_match_proto_config is called during connection reuse evaluation and lacks SMB share verification.

C3trigger supported

When a connection is reused for a different share, `libcurl` calls `smb_do`, advancing the multi state machine to the `DOING` phase since `smbc->share` is already set from the first request.

Read the reasoning

When a connection is reused, smbc retains the share from the first request. smb_do returns early because smbc->share is set, and smb_request_state proceeds with the old smbc->share but new req->path.

C4impact supported

The application silently fetches `old_share/new_path` instead of `new_share/new_path`, causing data spoofing.

Read the reasoning

The code path shows TREE_CONNECT is sent for the old share (smbc->share) while the file operation targets the new path (req->path), resulting in old_share/new_path access.

C5reproduction supported

The provided PoC script demonstrates that after fetching a file from `share1`, a subsequent request to `share2` returns the content from `share1` instead of `share2`.

Read the reasoning

The PoC is logically consistent with the code flaw. While I cannot execute it, the described behavior matches the code analysis: first request sets smbc->share, second request reuses connection and incorrectly accesses first share's content.

poc.sh:0

What the report says about other projects

not addressed The report recommends implementing an smb_conns_match function within lib/smb.c and calling it from url_match_proto_config logic (similar to FTP and SSH) in lib/url.c

not addressed The report recommends implementing an smb_conns_match function within lib/smb.c and calling it from url_match_proto_config logic (similar to FTP and SSH) in lib/url.c

not addressed The report recommends implementing an smb_conns_match function within lib/smb.c and calling it from url_match_proto_config logic (similar to FTP and SSH) in lib/url.c

Read the original report (excerpt)

Full report on HackerOne ↗

### Affected Version & File **Version:** curl 8.19.0 (and master up to `2e5d219205ebec64a66bfd29bba73dd5049c434f`), and likely older versions supporting SMB. **Files:** `lib/url.c` (at `url_match_proto_config`) and `lib/smb.c` ### What we found `libcurl` incorrectly reuses SMB connections across different shares on the same server, leading to data spoofing and access control bypass. When `libcurl` connects to an SMB share, the `smb_setup_connection` function parses the URL and saves the target share name into the connection's metadata (`smbc->share`). However, during connection reuse, `url_match_conn` only verifies the hostname, port, and credentials. It does not verify if the new URL's SMB share matches the existing connection's `smbc->share`. There is no `smb_conns_match` equivalent in `url_match_proto_config`. ```c // lib/url.c static bool url_match_proto_config(struct connectdata *conn, struct url_conn_match *m) { if(!url_match_http_version(conn, m)) return FALSE; #ifdef USE_SSH if(get_protocol_family(m->needle->scheme) & PROTO_FAMILY_SSH) { if(!ssh_config_matches(m->needle, conn)) return FALSE; } #endif #ifndef CURL_DISABLE_FTP else if(get_protocol_family(m->needle->scheme) & PROTO_FAMILY_FTP) { if(!ftp_conns_match(m->needle, conn)) return FALSE; } #endif /* VULNERABILITY: Missing SMB share matching here! */ return TRUE; } ``` Because of this, when the connection is reused for a different share, `libcurl` calls `smb_do`, which advances the multi state machine to the `DOING` phase since `smbc->share` is already set from the first request. Then, `smb_request_state` is invoked. It retrieves the old `smbc` (which retains the share name from the *first* request) and the new `req` (which contains the `req->path` from the *new* request). `libcurl` then sends a `TREE_CONNECT` for the old share, followed by an `NT_CREATE_ANDX` for the new path. As a result, `libcurl` silently fetches `old_share/new_path` instead of `new_share/new_path`. ### What we proved You can replicate this by setting up a Samba server (default configuration) and fetching different files sequentially via curl. The following PoC commands demonstrate the issue on a Linux system against the latest curl `8.19.0` build: ```bash # 1. Setup Samba server locally for the PoC sudo apt-get update && sudo apt-get install …

Source code

lib/ftp.clines 4462–4475 · definition of ftp_conns_match
4462bool ftp_conns_match(struct connectdata *needle, struct connectdata *conn)
C1: ftp_conns_match exists and compares FTP-specific connection metadata
4463{
4464 struct ftp_conn *nftpc = Curl_conn_meta_get(needle, CURL_META_FTP_CONN);
4465 struct ftp_conn *cftpc = Curl_conn_meta_get(conn, CURL_META_FTP_CONN);
4466 /* Also match ACCOUNT, ALTERNATIVE-TO-USER, USE_SSL and CCC options */
4467 if(!nftpc || !cftpc ||
4468 Curl_timestrcmp(nftpc->account, cftpc->account) ||
4469 Curl_timestrcmp(nftpc->alternative_to_user,
4470 cftpc->alternative_to_user) ||
4471 (nftpc->use_ssl != cftpc->use_ssl) ||
4472 (nftpc->ccc != cftpc->ccc))
4473 return FALSE;
4474 return TRUE;
4475}
lib/smb.clines 1195–1206 · definition of smb_do
1195static CURLcode smb_do(struct Curl_easy *data, bool *done)
C3: smb_do function checks if smbc->share is already set at line 1204 and returns CURLE_OK early
1196{
1197 struct connectdata *conn = data->conn;
1198 struct smb_conn *smbc = Curl_conn_meta_get(conn, CURL_META_SMB_CONN);
1199
1200 *done = FALSE;
1201 if(!smbc)
1202 return CURLE_FAILED_INIT;
1203 if(smbc->share)
1204 return CURLE_OK;
1205 return CURLE_URL_MALFORMAT;
1206}
lib/smb.clines 1000–1160 · definition of smb_request_state (truncated)
1000static CURLcode smb_request_state(struct Curl_easy *data, bool *done)
1001{
1002 struct connectdata *conn = data->conn;
1003 struct smb_conn *smbc = Curl_conn_meta_get(conn, CURL_META_SMB_CONN);
C3: smb_request_state retrieves smbc from connection metadata at line 1003
1004 struct smb_request *req = Curl_meta_get(data, CURL_META_SMB_EASY);
C3: smb_request_state retrieves req (with new request's path) from easy handle metadata at line 1004
1005 struct smb_header *h;
1006 enum smb_req_state next_state = SMB_DONE;
1007 unsigned short len;
1008 unsigned short off;
1009 CURLcode result;
1010 void *msg = NULL;
1011 const struct smb_nt_create_response *smb_m;
1012
1013 if(!smbc || !req)
1014 return CURLE_FAILED_INIT;
1015
1016 if(data->state.upload && (data->state.infilesize < 0)) {
1017 failf(data, "SMB upload needs to know the size up front");
1018 return CURLE_SEND_ERROR;
1019 }
1020
1021 /* Start the request */
1022 if(req->state == SMB_REQUESTING) {
1023 result = smb_send_tree_connect(data, smbc, req);
C4: smb_request_state calls smb_send_tree_connect with smbc (containing old share) at line 1023
1024 if(result) {
1025 connclose(conn, "SMB: failed to send tree connect message");
1026 return result;
1027 }
1028
1029 request_state(data, SMB_TREE_CONNECT);
1030 }
1031
1032 /* Send the previous message and check for a response */
1033 result = smb_send_and_recv(data, smbc, &msg);
1034 if(result && result != CURLE_AGAIN) {
1035 connclose(conn, "SMB: failed to communicate");
1036 return result;
1037 }
1038
1039 if(!msg)
1040 return CURLE_OK;
1041
1042 h = msg;
1043
1044 switch(req->state) {
1045 case SMB_TREE_CONNECT:
1046 if(h->status) {
1047 req->result = CURLE_REMOTE_FILE_NOT_FOUND;
1048 if(h->status == smb_swap32(SMB_ERR_NOACCESS))
1049 req->result = CURLE_REMOTE_ACCESS_DENIED;
1050 break;
1051 }
1052 req->tid = smb_swap16(h->tid);
1053 next_state = SMB_OPEN;
1054 break;
1055
1056 case SMB_OPEN:
1057 if(h->status || smbc->got < sizeof(struct smb_nt_create_response)) {
1058 req->result = CURLE_REMOTE_FILE_NOT_FOUND;
1059 if(h->status == smb_swap32(SMB_ERR_NOACCESS))
1060 req->result = CURLE_REMOTE_ACCESS_DENIED;
1061 next_state = SMB_TREE_DISCONNECT;
1062 break;
1063 }
1064 smb_m = (const struct smb_nt_create_response *)msg;
1065 req->fid = smb_swap16(smb_m->fid);
C4: Subsequent NT_CREATE_ANDX processing uses req->fid from the response, operating on the new request's path within the old share
1066 data->req.offset = 0;
1067 if(data->state.upload) {
1068 data->req.size = data->state.infilesize;
1069 Curl_pgrsSetUploadSize(data, data->req.size);
1070 next_state = SMB_UPLOAD;
1071 }
1072 else {
1073 data->req.size = smb_swap64(smb_m->end_of_file);
1074 if(data->req.size < 0) {
1075 req->result = CURLE_WEIRD_SERVER_REPLY;
1076 next_state = SMB_CLOSE;
1077 }
1078 else {
1079 Curl_pgrsSetDownloadSize(data, data->req.size);
1080 if(data->set.get_filetime)
1081 get_posix_time(&data->info.filetime, smb_m->last_change_time);
1082 next_state = SMB_DOWNLOAD;
1083 }
1084 }
1085 break;
1086
1087 case SMB_DOWNLOAD:
1088 if(h->status || smbc->got < sizeof(struct smb_header) + 15) {
1089 req->result = CURLE_RECV_ERROR;
1090 next_state = SMB_CLOSE;
1091 break;
1092 }
1093 len = Curl_read16_le((const unsigned char *)msg +
1094 sizeof(struct smb_header) + 11);
1095 off = Curl_read16_le((const unsigned char *)msg +
1096 sizeof(struct smb_header) + 13);
1097 if(len > 0) {
1098 if(off + sizeof(unsigned int) + len > smbc->got) {
1099 failf(data, "Invalid input packet");
1100 result = CURLE_RECV_ERROR;
1101 }
1102 else
1103 result = Curl_client_write(data, CLIENTWRITE_BODY,
1104 (char *)msg + off + sizeof(unsigned int),
1105 len);
1106 if(result) {
1107 req->result = result;
1108 next_state = SMB_CLOSE;
1109 break;
1110 }
1111 }
1112 data->req.offset += len;
1113 next_state = (len < MAX_PAYLOAD_SIZE) ? SMB_CLOSE : SMB_DOWNLOAD;
1114 break;
1115
1116 case SMB_UPLOAD:
1117 if(h->status || smbc->got < sizeof(struct smb_header) + 7) {
1118 req->result = CURLE_UPLOAD_FAILED;
1119 next_state = SMB_CLOSE;
1120 break;
1121 }
1122 len = Curl_read16_le((const unsigned char *)msg +
1123 sizeof(struct smb_header) + 5);
1124 data->req.bytecount += len;
1125 data->req.offset += len;
1126 Curl_pgrs_upload_inc(data, len);
1127 if(data->req.bytecount >= data->req.size)
1128 next_state = SMB_CLOSE;
1129 else
1130 next_state = SMB_UPLOAD;
1131 break;
1132
1133 case SMB_CLOSE:
1134 /* We do not care if the close failed, proceed to tree disconnect anyway */
1135 next_state = SMB_TREE_DISCONNECT;
1136 break;
1137
1138 case SMB_TREE_DISCONNECT:
1139 next_state = SMB_DONE;
1140 break;
1141
1142 default:
1143 smb_pop_message(smbc);
1144 return CURLE_OK; /* ignore */
1145 }
1146
1147 smb_pop_message(smbc);
1148
1149 switch(next_state) {
1150 case SMB_OPEN:
1151 result = smb_send_open(data, smbc, req);
1152 break;
1153
1154 case SMB_DOWNLOAD:
1155 result = smb_send_read(data, smbc, req);
1156 break;
1157
1158 case SMB_UPLOAD:
1159 result = smb_send_write(data, smbc, req);
1160 break;
lib/url.clines 700–709 · definition of ssh_config_matches
700static bool ssh_config_matches(struct connectdata *one,
C1: ssh_config_matches exists and compares SSH-specific connection metadata
701 struct connectdata *two)
702{
703 struct ssh_conn *sshc1, *sshc2;
704
705 sshc1 = Curl_conn_meta_get(one, CURL_META_SSH_CONN);
706 sshc2 = Curl_conn_meta_get(two, CURL_META_SSH_CONN);
707 return sshc1 && sshc2 && Curl_safecmp(sshc1->rsa, sshc2->rsa) &&
708 Curl_safecmp(sshc1->rsa_pub, sshc2->rsa_pub);
709}
lib/url.clines 1208–1268 · definition of url_match_conn
1208static bool url_match_conn(struct connectdata *conn, void *userdata)
1209{
1210 struct url_conn_match *m = userdata;
1211 /* Check if `conn` can be used for transfer `m->data` */
1212
1213 /* general connect config setting match? */
1214 if(!url_match_connect_config(conn, m))
1215 return FALSE;
1216
1217 /* match for destination and protocol? */
1218 if(!url_match_destination(conn, m))
1219 return FALSE;
1220
1221 if(!url_match_fully_connected(conn, m))
1222 return FALSE;
1223
1224 if(!url_match_multiplex_needs(conn, m))
1225 return FALSE;
1226
1227 if(!url_match_ssl_use(conn, m))
1228 return FALSE;
1229 if(!url_match_proxy_use(conn, m))
1230 return FALSE;
1231 if(!url_match_ssl_config(conn, m))
1232 return FALSE;
1233
1234 if(!url_match_http_multiplex(conn, m))
1235 return FALSE;
1236 else if(m->wait_pipe)
1237 /* we decided to wait on PIPELINING */
1238 return TRUE;
1239
1240 if(!url_match_auth(conn, m))
1241 return FALSE;
1242
1243 if(!url_match_proto_config(conn, m))
C2: url_match_conn calls url_match_proto_config at line 1243 as part of connection reuse decision
1244 return FALSE;
1245
1246 if(!url_match_auth_ntlm(conn, m))
1247 return FALSE;
1248 else if(m->force_reuse)
1249 return TRUE;
1250
1251 if(!url_match_auth_nego(conn, m))
1252 return FALSE;
1253 else if(m->force_reuse)
1254 return TRUE;
1255
1256 if(!url_match_multiplex_limits(conn, m))
1257 return FALSE;
1258
1259 if(!CONN_INUSE(conn) && Curl_conn_seems_dead(conn, m->data)) {
1260 /* remove and disconnect. */
1261 Curl_conn_terminate(m->data, conn, FALSE);
1262 return FALSE;
1263 }
1264
1265 /* conn matches our needs. */
1266 m->found = conn;
1267 return TRUE;
1268}
lib/url.clines 977–996 · definition of url_match_proto_config
977static bool url_match_proto_config(struct connectdata *conn,
C1: url_match_proto_config lacks SMB share matching while having protocol-specific matching for SSH (lines 984-987) and FTP (lines 990-993)
C2: url_match_proto_config function definition shows no SMB share matching logic
978 struct url_conn_match *m)
979{
980 if(!url_match_http_version(conn, m))
981 return FALSE;
982
983#ifdef USE_SSH
984 if(get_protocol_family(m->needle->scheme) & PROTO_FAMILY_SSH) {
985 if(!ssh_config_matches(m->needle, conn))
986 return FALSE;
987 }
988#endif
989#ifndef CURL_DISABLE_FTP
990 else if(get_protocol_family(m->needle->scheme) & PROTO_FAMILY_FTP) {
991 if(!ftp_conns_match(m->needle, conn))
992 return FALSE;
993 }
994#endif
995 return TRUE;
996}
lib/url.clines 149–154 · definition of get_protocol_family
Show 6 lines (not cited by the judge)
149static curl_prot_t get_protocol_family(const struct Curl_scheme *s)
150{
151 DEBUGASSERT(s);
152 DEBUGASSERT(s->family);
153 return s->family;
154}
lib/smb.clines 444–464 · definition of smb_setup_connection
Show 21 lines (not cited by the judge)
444static CURLcode smb_setup_connection(struct Curl_easy *data,
445 struct connectdata *conn)
446{
447 struct smb_conn *smbc;
448 struct smb_request *req;
449
450 /* Initialize the connection state */
451 smbc = curlx_calloc(1, sizeof(*smbc));
452 if(!smbc ||
453 Curl_conn_meta_set(conn, CURL_META_SMB_CONN, smbc, smb_conn_dtor))
454 return CURLE_OUT_OF_MEMORY;
455
456 /* Initialize the request state */
457 req = curlx_calloc(1, sizeof(*req));
458 if(!req ||
459 Curl_meta_set(data, CURL_META_SMB_EASY, req, smb_easy_dtor))
460 return CURLE_OUT_OF_MEMORY;
461
462 /* Parse the URL path */
463 return smb_parse_url_path(data, smbc, req);
464}
lib/url.clines 940–971 · definition of url_match_http_version
Show 32 lines (not cited by the judge)
940static bool url_match_http_version(struct connectdata *conn,
941 struct url_conn_match *m)
942{
943 /* If looking for HTTP and the HTTP versions allowed do not include
944 * the HTTP version of conn, continue looking. */
945 if((m->needle->scheme->protocol & PROTO_FAMILY_HTTP)) {
946 switch(Curl_conn_http_version(m->data, conn)) {
947 case 30:
948 if(!(m->data->state.http_neg.allowed & CURL_HTTP_V3x)) {
949 DEBUGF(infof(m->data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T
950 ", we do not want h3", conn->connection_id));
951 return FALSE;
952 }
953 break;
954 case 20:
955 if(!(m->data->state.http_neg.allowed & CURL_HTTP_V2x)) {
956 DEBUGF(infof(m->data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T
957 ", we do not want h2", conn->connection_id));
958 return FALSE;
959 }
960 break;
961 default:
962 if(!(m->data->state.http_neg.allowed & CURL_HTTP_V1x)) {
963 DEBUGF(infof(m->data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T
964 ", we do not want h1", conn->connection_id));
965 return FALSE;
966 }
967 break;
968 }
969 }
970 return TRUE;
971}