✓ProveIt
Examples / Report #1223882 Saved analysis

CVE-2021-22925: TELNET stack contents disclosure again

curl · 2021-06-1114.3s analysis · $0.017 in model calls

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

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?
  • 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.

1 · Grounding
grounded
no model · 11 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2067 tokens · $0.0002
3 · Evidence
3 code snippets
real curl source at 6b951a692881 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
9914 tokens · 12.0s · $0.0167
5 · Receipts
11 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 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

C1flaw supported

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.

C2location supported

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.

C3trigger supported

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.

C4impact supported

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.

C5reproduction supported

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)

Full report on HackerOne ↗

## Summary: CVE-2021-22898: TELNET stack contents disclosure (#1176461) issue was recently reported for curl and it was addressed in curl 7.77.0: https://curl.se/docs/CVE-2021-22898.html https://github.com/curl/curl/commit/39ce47f219b09c380b81f89fe54ac586c8db6bde https://hackerone.com/reports/1176461 However, the fix applied is not correct and does not completely address the issue. It helps in cases when long environment variable name is used (`'a'*256 + ',b'`), but not when the name is short and only the value is long (`'a,' + 'b'*256`, which is the example mentioned in the curl project advisory). ## Steps To Reproduce: Follow the steps form #1176461, only use NEW_ENV option with short name and long value, such as: ``` $ curl telnet://127.0.0.1:23 -t NEW_ENV=`python -c "print('a,' + 'b'*256)"` ``` ## Supporting Material/References: When parsing NEW_ENV option value with short name and long value, sscanf() returns 2, as it writes to both `varname` and `varval`, even though the data in `varval` is truncated. Hence such variable is not skipped and is added to the `temp[]` buffer. However, the `len` counter which tracks the amount of data that was already written to `temp[]` is not updated based on the data written to the buffer in the `msnprintf()`call, but rather based on the length of the original unparsed data that is stored in `tmplen`. The relevant code is here: https://github.com/curl/curl/blob/curl-7_77_0/lib/telnet.c#L926-L929 When value stored in `varval` is truncated, `len` is increased too much and a chunk of uninitialized memory is created in `temp[]`. The `len` should only be incremented by `strlen(varname) + strlen(varval) + 2`. I wonder if the original fix should be preserved or re-worked. In addition to not fixing the info leak problem properly, it also causes certain valid option values to be ignored and not sent to a server any more. Rejected values are of the forms `NEW_ENV=a` or `NEW_ENV=a,`. At least the second one seems like an obviously valid way to set variable `a` to an empty string. RFC 1572 defines that environment variable can be sent with empty value and hence `NEW_ENV=a,` should remain supported. It also defines that variable can be sent with no value, making `NEW_ENV=a` a valid option as well. Note that curl prior to 7.77.0 actually did handle `NEW_ENV=a` that way, but it looks more like an unintended side e …

Source code

lib/telnet.clines 901–954 · lines 926-929 cited by the report
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
954static void sendsuboption(struct Curl_easy *data, int option)
lib/telnet.clines 773–868 · lines 799-800 cited by the report; definition of check_telnet_options
773static 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}
lib/curl_printf.hlines 42–42 · definition of msnprintf
Show 1 lines (not cited by the judge)
42# define msnprintf curl_msnprintf