CVE-2021-22925: TELNET stack contents disclosure again
What the report claims
The fix for CVE-2021-22898 in curl 7.77.0 does not fully address the TELNET stack contents disclosure issue; it only mitigates cases with long environment variable names but fails to handle short names with long values, leading to uninitialized memory leaks and potential rejection of valid option values.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2021-22925 TELNET stack contents disclosure again
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?
- The provided code at lib/telnet.c lines 921-931 shows a clear path where sscanf truncates varval to 127 bytes but returns 2, then len is incremented by the original tmplen (up to 256) instead of the actual written length.
- This creates a gap in the stack-allocated temp[] buffer that is sent to the network via swrite, disclosing uninitialized stack memory.
- The trigger (NEW_ENV with short name and long value) is reachable by any user controlling curl's -t/--telnet-option argument.
- The deterministic checks confirm the cited functions and lines exist in the target revision (curl-7_77_0).
How this analysis ran 5 claims · 11 citations verified
Checked against curl permalink curl-7_77_0 (6b951a692881). Dev 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 analysis. The code at lib/telnet.c:921-931 confirms the fix for CVE-2021-22898 is incomplete: sscanf truncates varval to 127 bytes but returns 2, while len is incremented by the original tmplen (up to 256). This leaves uninitialized stack memory in temp[] which is sent to the server. The reproduction command is valid. We will address this and also consider the RFC 1572 compliance for empty-value and no-value variables.
Inspect all claims and source code 5 claims · 3 code excerpts
Report claims
sscanf() returns 2, writing to both varname and varval even when varval is truncated, causing len to be incremented incorrectly
Read the reasoning
The sscanf call at line 925 uses width-limited formats that truncate varval to 127 bytes but still return 2 on success. The subsequent len increment at line 929 uses tmplen (strlen of original v->data + 1), which can be up to 256 bytes from check_telnet_options, while msnprintf at lines 926-928 writes only the truncated strings.
The issue occurs in lib/telnet.c around lines 926-929 where the len counter is updated based on tmplen instead of the actual written data length
Read the reasoning
The code at lines 922 and 929 confirms len is updated based on tmplen (the original data length) rather than the return value of msnprintf or the actual lengths of varname and varval after truncation.
Using NEW_ENV with a short name and long value such as 'a,' + 'b'*256 triggers the bug
Read the reasoning
A NEW_ENV value with short name (e.g., 'a') and long value (256 'b's) passes check_telnet_options (truncated to 255 chars), then in the loop at line 921-931, sscanf matches both fields (returning 2) but truncates varval to 127 chars, while tmplen remains ~256, causing the len over-increment.
Uninitialized stack memory is leaked via the temp[] buffer due to incorrect len increment
Read the reasoning
temp is a stack buffer (line 916). The gap between actual msnprintf output (truncated varname/varval) and the inflated len means swrite at line 936 transmits uninitialized stack memory to the telnet server.
The command curl telnet://127.0.0.1:23 -t NEW_ENV=`python -c "print('a,' + 'b'*256)"` reproduces the issue
Read the reasoning
The command produces a NEW_ENV option with a short name and 256-char value. check_telnet_options accepts it (truncating to 255 chars), stores it in telnet_vars, and the sendsuboption loop processes it, triggering the len mismatch.
What the report says about other projects
not addressed RFC 1572 defines that environment variables can be sent with empty values and that a variable can be sent with no value, making NEW_ENV=a, and NEW_ENV=a valid options
Read the original report (excerpt)
Source code
| 901 | printsub(data, '>', &temp[2], len-2); |
| 902 | break; |
| 903 | case CURL_TELOPT_XDISPLOC: |
| 904 | len = strlen(tn->subopt_xdisploc) + 4 + 2; |
| 905 | msnprintf((char *)temp, sizeof(temp), |
| 906 | "%c%c%c%c%s%c%c", CURL_IAC, CURL_SB, CURL_TELOPT_XDISPLOC, |
| 907 | CURL_TELQUAL_IS, tn->subopt_xdisploc, CURL_IAC, CURL_SE); |
| 908 | bytes_written = swrite(conn->sock[FIRSTSOCKET], temp, len); |
| 909 | if(bytes_written < 0) { |
| 910 | err = SOCKERRNO; |
| 911 | failf(data,"Sending data failed (%d)",err); |
| 912 | } |
| 913 | printsub(data, '>', &temp[2], len-2); |
| 914 | break; |
| 915 | case CURL_TELOPT_NEW_ENVIRON: |
| 916 | msnprintf((char *)temp, sizeof(temp), |
| C4: temp buffer allocated on stack | |
| 917 | "%c%c%c%c", CURL_IAC, CURL_SB, CURL_TELOPT_NEW_ENVIRON, |
| 918 | CURL_TELQUAL_IS); |
| 919 | len = 4; |
| 920 | |
| 921 | for(v = tn->telnet_vars; v; v = v->next) { |
| C5: Loop processes each telnet_vars entry including the NEW_ENV value | |
| 922 | size_t tmplen = (strlen(v->data) + 1); |
| C2: tmplen = strlen(v->data) + 1 captures original unparsed length | |
| 923 | /* Add the variable only if it fits */ |
| 924 | if(len + tmplen < (int)sizeof(temp)-6) { |
| 925 | if(sscanf(v->data, "%127[^,],%127s", varname, varval) == 2) { |
| C1: sscanf with format "%127[^,],%127s" returns 2 when both varname and varval match, even if varval is truncated to 127 chars | |
| C3: sscanf then truncates varval to 127 chars but returns 2 | |
| 926 | msnprintf((char *)&temp[len], sizeof(temp) - len, |
| 927 | "%c%s%c%s", CURL_NEW_ENV_VAR, varname, |
| 928 | CURL_NEW_ENV_VALUE, varval); |
| 929 | len += tmplen; |
| C1: len is incremented by tmplen (original data length) not by actual bytes written by msnprintf | |
| C2: len += tmplen uses that original length instead of actual written length | |
| C4: len incremented beyond actual written data | |
| 930 | } |
| 931 | } |
| 932 | } |
| 933 | msnprintf((char *)&temp[len], sizeof(temp) - len, |
| 934 | "%c%c", CURL_IAC, CURL_SE); |
| 935 | len += 2; |
| 936 | bytes_written = swrite(conn->sock[FIRSTSOCKET], temp, len); |
| C4: swrite sends temp buffer with inflated len | |
| 937 | if(bytes_written < 0) { |
| 938 | err = SOCKERRNO; |
| 939 | failf(data,"Sending data failed (%d)",err); |
| 940 | } |
| 941 | printsub(data, '>', &temp[2], len-2); |
| 942 | break; |
| 943 | } |
| 944 | return; |
| 945 | } |
| 946 | |
| 947 | |
| 948 | /* |
| 949 | * sendsuboption() |
| 950 | * |
| 951 | * Send suboption information to the server side. |
| 952 | */ |
| 953 | |
| 954 | static void sendsuboption(struct Curl_easy *data, int option) |
| 773 | static CURLcode check_telnet_options(struct Curl_easy *data) |
| 774 | { |
| 775 | struct curl_slist *head; |
| 776 | struct curl_slist *beg; |
| 777 | char option_keyword[128] = ""; |
| 778 | char option_arg[256] = ""; |
| 779 | struct TELNET *tn = data->req.p.telnet; |
| 780 | struct connectdata *conn = data->conn; |
| 781 | CURLcode result = CURLE_OK; |
| 782 | int binary_option; |
| 783 | |
| 784 | /* Add the user name as an environment variable if it |
| 785 | was given on the command line */ |
| 786 | if(conn->bits.user_passwd) { |
| 787 | msnprintf(option_arg, sizeof(option_arg), "USER,%s", conn->user); |
| 788 | beg = curl_slist_append(tn->telnet_vars, option_arg); |
| 789 | if(!beg) { |
| 790 | curl_slist_free_all(tn->telnet_vars); |
| 791 | tn->telnet_vars = NULL; |
| 792 | return CURLE_OUT_OF_MEMORY; |
| 793 | } |
| 794 | tn->telnet_vars = beg; |
| 795 | tn->us_preferred[CURL_TELOPT_NEW_ENVIRON] = CURL_YES; |
| 796 | } |
| 797 | |
| 798 | for(head = data->set.telnet_options; head; head = head->next) { |
| 799 | if(sscanf(head->data, "%127[^= ]%*[ =]%255s", |
| C3: check_telnet_options parses option_arg with %255s, allowing up to 255-char values | |
| C5: Option parsing accepts NEW_ENV= with up to 255-char argument | |
| 800 | option_keyword, option_arg) == 2) { |
| 801 | |
| 802 | /* Terminal type */ |
| 803 | if(strcasecompare(option_keyword, "TTYPE")) { |
| 804 | strncpy(tn->subopt_ttype, option_arg, 31); |
| 805 | tn->subopt_ttype[31] = 0; /* String termination */ |
| 806 | tn->us_preferred[CURL_TELOPT_TTYPE] = CURL_YES; |
| 807 | continue; |
| 808 | } |
| 809 | |
| 810 | /* Display variable */ |
| 811 | if(strcasecompare(option_keyword, "XDISPLOC")) { |
| 812 | strncpy(tn->subopt_xdisploc, option_arg, 127); |
| 813 | tn->subopt_xdisploc[127] = 0; /* String termination */ |
| 814 | tn->us_preferred[CURL_TELOPT_XDISPLOC] = CURL_YES; |
| 815 | continue; |
| 816 | } |
| 817 | |
| 818 | /* Environment variable */ |
| 819 | if(strcasecompare(option_keyword, "NEW_ENV")) { |
| 820 | beg = curl_slist_append(tn->telnet_vars, option_arg); |
| 821 | if(!beg) { |
| 822 | result = CURLE_OUT_OF_MEMORY; |
| 823 | break; |
| 824 | } |
| 825 | tn->telnet_vars = beg; |
| 826 | tn->us_preferred[CURL_TELOPT_NEW_ENVIRON] = CURL_YES; |
| 827 | continue; |
| 828 | } |
| 829 | |
| 830 | /* Window Size */ |
| 831 | if(strcasecompare(option_keyword, "WS")) { |
| 832 | if(sscanf(option_arg, "%hu%*[xX]%hu", |
| 833 | &tn->subopt_wsx, &tn->subopt_wsy) == 2) |
| 834 | tn->us_preferred[CURL_TELOPT_NAWS] = CURL_YES; |
| 835 | else { |
| 836 | failf(data, "Syntax error in telnet option: %s", head->data); |
| 837 | result = CURLE_TELNET_OPTION_SYNTAX; |
| 838 | break; |
| 839 | } |
| 840 | continue; |
| 841 | } |
| 842 | |
| 843 | /* To take care or not of the 8th bit in data exchange */ |
| 844 | if(strcasecompare(option_keyword, "BINARY")) { |
| 845 | binary_option = atoi(option_arg); |
| 846 | if(binary_option != 1) { |
| 847 | tn->us_preferred[CURL_TELOPT_BINARY] = CURL_NO; |
| 848 | tn->him_preferred[CURL_TELOPT_BINARY] = CURL_NO; |
| 849 | } |
| 850 | continue; |
| 851 | } |
| 852 | |
| 853 | failf(data, "Unknown telnet option %s", head->data); |
| 854 | result = CURLE_UNKNOWN_OPTION; |
| 855 | break; |
| 856 | } |
| 857 | failf(data, "Syntax error in telnet option: %s", head->data); |
| 858 | result = CURLE_TELNET_OPTION_SYNTAX; |
| 859 | break; |
| 860 | } |
| 861 | |
| 862 | if(result) { |
| 863 | curl_slist_free_all(tn->telnet_vars); |
| 864 | tn->telnet_vars = NULL; |
| 865 | } |
| 866 | |
| 867 | return result; |
| 868 | } |
Show 1 lines (not cited by the judge)
| 42 | # define msnprintf curl_msnprintf |