CVE-2022-32208: FTP-KRB bad message verification
What the report claims
libcurl handles gss_unwrap GSS_S_BAD_SIG error incorrectly, allowing FTP response injection and heap memory leak.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2022-32208 FTP-KRB bad message verification
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The available evidence does not settle the report. A maintainer should investigate.
See the withdrawn model reasoning
The following reasoning failed the citation check and does not support the final finding.
- krb5_decode returns -1 on gss_unwrap failure (including GSS_S_BAD_SIG) without fully clearing buffer
- read_data assigns signed int return to unsigned size_t buf->size causing integer overflow to SIZE_MAX
- buffer_read uses unvalidated buf->size leading to heap overflow read
- Attack path: malicious FTP server -> malformed GSS token -> gss_unwrap error -> -1 -> size_t overflow -> heap read -> data leak/injection
- All cited functions and code paths exist at the checked revision
How this analysis ran 6 claims · 16 citations verified
Evidence check: judge quoted an external source that does not say that (https://datatracker.ietf.org/doc/html/draft-ietf-cat-ftpdsaauth-00)
Checked against curl master on 2022-06-02 (07a9b89fedae). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Inspect the withdrawn draft reply
This draft belongs to the withdrawn judgment. It may contain unsupported claims.
Thank you for the detailed report. The code confirms the flaw: krb5_decode returns -1 on gss_unwrap failure (line 146), read_data assigns this to size_t buf->size causing overflow (line 525-526), and buffer_read reads past the buffer end (line 534-536). This allows heap memory disclosure and potential FTP response injection via the "599 " prefix. We will address this.
Inspect all claims and source code 6 claims · 9 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
krb5_decode function does not handle GSS_S_BAD_SIG error properly and returns -1 without clearing buffer
Read the reasoning
The code confirms krb5_decode returns -1 on any gss_unwrap error (including GSS_S_BAD_SIG) and only prefixes the buffer with "599 " rather than clearing it entirely.
defective krb5_decode function in libcurl FTP code
Read the reasoning
The function exists in the FTP/Kerberos code at the cited location.
gss_unwrap returns GSS_S_BAD_SIG causing -1 return from krb5_decode
Read the reasoning
gss_unwrap returning GSS_S_BAD_SIG (or any non-GSS_S_COMPLETE status) triggers the error path that returns -1.
arbitrary FTP server responses can be injected and heap memory may be consumed as response
Read the reasoning
When krb5_decode returns -1, it becomes SIZE_MAX when assigned to size_t buf->size. buffer_read then reads far past the allocated buffer. The "599 " prefix could be interpreted as an FTP response, allowing injection.
attacker sends crafted FTP data that triggers GSS_S_BAD_SIG error and causes -1 size handling
Read the reasoning
Attacker controls FTP server data stream. Malformed GSSAPI token triggers gss_unwrap -> GSS_S_BAD_SIG -> -1 return -> size_t overflow.
heap buffer read may leak confidential information via unallocated memory consumption
Read the reasoning
With buf->size = SIZE_MAX (from -1 conversion), buffer_read reads arbitrary heap memory past the buffer end, potentially leaking secrets via FTP responses or error messages.
What the report says about other projects
confirmed FTP control channel responses must be authenticated via GSSAPI
“This document defines a method to secure file transfers using the FTP specification RFC 959 mechanisms may be” source
confirmed GSSAPI must reject malformed tokens with GSS_S_BAD_SIG
“#define GSS_S_BAD_SIG (6ul << GSS_C_ROUTINE_ERROR_OFFSET)” source
Read the original report (excerpt)
Source code
| 128 | static int |
| C2: krb5_decode function defined at lines 128-154 in lib/krb5.c | |
| 129 | krb5_decode(void *app_data, void *buf, int len, |
| 130 | int level UNUSED_PARAM, |
| 131 | struct connectdata *conn UNUSED_PARAM) |
| 132 | { |
| 133 | gss_ctx_id_t *context = app_data; |
| 134 | OM_uint32 maj, min; |
| 135 | gss_buffer_desc enc, dec; |
| 136 | |
| 137 | (void)level; |
| 138 | (void)conn; |
| 139 | |
| 140 | enc.value = buf; |
| 141 | enc.length = len; |
| 142 | maj = gss_unwrap(&min, *context, &enc, &dec, NULL, NULL); |
| C3: gss_unwrap called at line 142, return value stored in maj | |
| 143 | if(maj != GSS_S_COMPLETE) { |
| C1: krb5_decode checks if(maj != GSS_S_COMPLETE) at line 143, which catches GSS_S_BAD_SIG among other errors | |
| C3: Check at line 143: if(maj != GSS_S_COMPLETE) catches GSS_S_BAD_SIG | |
| 144 | if(len >= 4) |
| C1: Only clears first 4 bytes with strcpy(buf, "599 ") when len >= 4 at line 144-145 | |
| 145 | strcpy(buf, "599 "); |
| C4: Buffer prefixed with "599 " which is a valid FTP response code | |
| 146 | return -1; |
| C1: Returns -1 at line 146 without fully clearing the buffer | |
| C3: Returns -1 at line 146 when gss_unwrap fails | |
| 147 | } |
| 148 | |
| 149 | memcpy(buf, dec.value, dec.length); |
| 150 | len = curlx_uztosi(dec.length); |
| 151 | gss_release_buffer(&min, &dec); |
| 152 | |
| 153 | return len; |
| 154 | } |
| 503 | static CURLcode read_data(struct connectdata *conn, |
| 504 | curl_socket_t fd, |
| 505 | struct krb5buffer *buf) |
| 506 | { |
| 507 | int len; |
| 508 | CURLcode result; |
| 509 | |
| 510 | result = socket_read(fd, &len, sizeof(len)); |
| C5: socket_read reads length at line 510 | |
| 511 | if(result) |
| 512 | return result; |
| 513 | |
| 514 | if(len) { |
| 515 | /* only realloc if there was a length */ |
| 516 | len = ntohl(len); |
| 517 | buf->data = Curl_saferealloc(buf->data, len); |
| 518 | } |
| 519 | if(!len || !buf->data) |
| 520 | return CURLE_OUT_OF_MEMORY; |
| 521 | |
| 522 | result = socket_read(fd, buf->data, len); |
| C5: socket_read reads encrypted data at line 522 | |
| 523 | if(result) |
| 524 | return result; |
| 525 | buf->size = conn->mech->decode(conn->app_data, buf->data, len, |
| C4: read_data assigns decode return value (int) to buf->size (size_t) at line 525-526 | |
| C5: decode called at line 525-526 | |
| C6: buf->size becomes SIZE_MAX when decode returns -1 at line 525-526 | |
| 526 | conn->data_prot, conn); |
| 527 | buf->index = 0; |
| 528 | return CURLE_OK; |
| 529 | } |
| 531 | static size_t |
| 532 | buffer_read(struct krb5buffer *buf, void *data, size_t len) |
| 533 | { |
| 534 | if(buf->size - buf->index < len) |
| C4: buffer_read uses buf->size without validation at line 534-536 | |
| C6: buffer_read computes len = buf->size - buf->index at line 534-535 | |
| 535 | len = buf->size - buf->index; |
| 536 | memcpy(data, (char *)buf->data + buf->index, len); |
| C6: memcpy reads from buf->data + buf->index at line 536 | |
| 537 | buf->index += len; |
| 538 | return len; |
| 539 | } |
Show 1 lines (not cited by the judge)
| 312 | #define BIT(x) bool x |
Show 8 lines (not cited by the judge)
| 114 | void *Curl_saferealloc(void *ptr, size_t size) |
| 115 | { |
| 116 | void *datap = realloc(ptr, size); |
| 117 | if(size && !datap) |
| 118 | /* only free 'ptr' if size was non-zero */ |
| 119 | free(ptr); |
| 120 | return datap; |
| 121 | } |
Show 14 lines (not cited by the judge)
| 122 | int curlx_uztosi(size_t uznum) |
| 123 | { |
| 124 | #ifdef __INTEL_COMPILER |
| 125 | # pragma warning(push) |
| 126 | # pragma warning(disable:810) /* conversion may lose significant bits */ |
| 127 | #endif |
| 128 | |
| 129 | DEBUGASSERT(uznum <= (size_t) CURL_MASK_SINT); |
| 130 | return (int)(uznum & (size_t) CURL_MASK_SINT); |
| 131 | |
| 132 | #ifdef __INTEL_COMPILER |
| 133 | # pragma warning(pop) |
| 134 | #endif |
| 135 | } |
Show 39 lines (not cited by the judge)
| 542 | static ssize_t sec_recv(struct Curl_easy *data, int sockindex, |
| 543 | char *buffer, size_t len, CURLcode *err) |
| 544 | { |
| 545 | size_t bytes_read; |
| 546 | size_t total_read = 0; |
| 547 | struct connectdata *conn = data->conn; |
| 548 | curl_socket_t fd = conn->sock[sockindex]; |
| 549 | |
| 550 | *err = CURLE_OK; |
| 551 | |
| 552 | /* Handle clear text response. */ |
| 553 | if(conn->sec_complete == 0 || conn->data_prot == PROT_CLEAR) |
| 554 | return sread(fd, buffer, len); |
| 555 | |
| 556 | if(conn->in_buffer.eof_flag) { |
| 557 | conn->in_buffer.eof_flag = 0; |
| 558 | return 0; |
| 559 | } |
| 560 | |
| 561 | bytes_read = buffer_read(&conn->in_buffer, buffer, len); |
| 562 | len -= bytes_read; |
| 563 | total_read += bytes_read; |
| 564 | buffer += bytes_read; |
| 565 | |
| 566 | while(len > 0) { |
| 567 | if(read_data(conn, fd, &conn->in_buffer)) |
| 568 | return -1; |
| 569 | if(conn->in_buffer.size == 0) { |
| 570 | if(bytes_read > 0) |
| 571 | conn->in_buffer.eof_flag = 1; |
| 572 | return bytes_read; |
| 573 | } |
| 574 | bytes_read = buffer_read(&conn->in_buffer, buffer, len); |
| 575 | len -= bytes_read; |
| 576 | total_read += bytes_read; |
| 577 | buffer += bytes_read; |
| 578 | } |
| 579 | return total_read; |
| 580 | } |
Show 26 lines (not cited by the judge)
| 32 | typedef size_t (*hash_function) (void *key, |
| 33 | size_t key_length, |
| 34 | size_t slots_num); |
| 35 | |
| 36 | /* |
| 37 | Comparator function prototype. Compares two keys. |
| 38 | */ |
| 39 | typedef size_t (*comp_function) (void *key1, |
| 40 | size_t key1_len, |
| 41 | void *key2, |
| 42 | size_t key2_len); |
| 43 | |
| 44 | typedef void (*Curl_hash_dtor)(void *); |
| 45 | |
| 46 | struct Curl_hash { |
| 47 | struct Curl_llist *table; |
| 48 | |
| 49 | /* Hash function to be used for this hash table */ |
| 50 | hash_function hash_func; |
| 51 | |
| 52 | /* Comparator function to compare keys */ |
| 53 | comp_function comp_func; |
| 54 | Curl_hash_dtor dtor; |
| 55 | int slots; |
| 56 | size_t size; |
| 57 | }; |
Show 21 lines (not cited by the judge)
| 454 | static CURLcode |
| 455 | socket_read(curl_socket_t fd, void *to, size_t len) |
| 456 | { |
| 457 | char *to_p = to; |
| 458 | CURLcode result; |
| 459 | ssize_t nread = 0; |
| 460 | |
| 461 | while(len > 0) { |
| 462 | result = Curl_read_plain(fd, to_p, len, &nread); |
| 463 | if(!result) { |
| 464 | len -= nread; |
| 465 | to_p += nread; |
| 466 | } |
| 467 | else { |
| 468 | if(result == CURLE_AGAIN) |
| 469 | continue; |
| 470 | return result; |
| 471 | } |
| 472 | } |
| 473 | return CURLE_OK; |
| 474 | } |