CVE-2026-5773: wrong reuse of SMB connection
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
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.
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
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.
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.
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.
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.
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.
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)
Source code
| 4462 | bool 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 | } |
| 1195 | static 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 | } |
| 1000 | static 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; |
| 700 | static 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 | } |
| 1208 | static 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 | } |
| 977 | static 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 | } |
Show 6 lines (not cited by the judge)
| 149 | static curl_prot_t get_protocol_family(const struct Curl_scheme *s) |
| 150 | { |
| 151 | DEBUGASSERT(s); |
| 152 | DEBUGASSERT(s->family); |
| 153 | return s->family; |
| 154 | } |
Show 21 lines (not cited by the judge)
| 444 | static 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 | } |
Show 32 lines (not cited by the judge)
| 940 | static 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 | } |