CVE-2021-22898: TELNET stack contents disclosure
What the report claims
The lib/telnet.c suboption function incorrectly checks the return value of sscanf, allowing uninitialized stack data to be leaked when parsing environment variables.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2021-22898 TELNET stack contents disclosure
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.
- The sscanf return-value check is indeed incorrect (accepts 1 instead of requiring 2), confirming a logic bug.
- However, varval is initialized to empty string at function entry and only ever contains attacker-supplied values; it never holds uninitialized stack data.
- The temp buffer is written sequentially from offset 0 with no gaps; swrite sends only the initialized prefix.
- Since the attacker controls all input via CURLOPT_TELNETOPTIONS, no confidential data can be leaked through this path.
- The reported stack-content disclosure is not supported by the provided code.
How this analysis ran 5 claims · 8 citations verified
Evidence check: judge said likely_invalid without a code citation contradicting a central claim
Checked against curl master on 2021-04-27 (2e23f3b8d54c). 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 sscanf check at lib/telnet.c:924 does accept a return value of 1, which is a logic bug. However, varval is initialized to an empty string (line 883) and only ever holds values from attacker-controlled input. The temp buffer is written sequentially from the start with no gaps, and swrite sends only the initialized portion. Because the attacker supplies all NEW_ENV values, no stack contents or confidential data can be leaked. This appears to be a correctness bug, not a security vulnerability. Could you clarify how you believe uninitialized stack data reaches the output buffer?
Inspect all claims and source code 5 claims · 1 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
sscanf(v->data, "%127[^,],%127s", varname, varval) does not verify that both fields are parsed
Read the reasoning
The sscanf return value is checked for truthiness rather than equality to 2, so the branch executes when only one field is parsed.
The flaw occurs in the suboption function of lib/telnet.c
Read the reasoning
The flaw is in the suboption function in lib/telnet.c as reported.
An attacker can control environment variables passed via CURLOPT_TELNETOPTIONS and cause malformed input to be processed
Read the reasoning
The attacker controls the telnet options passed via CURLOPT_TELNETOPTIONS, so they can craft malformed input strings.
Uninitialized stack contents may be included in the generated output, potentially leaking sensitive information
Read the reasoning
The temp buffer is written sequentially from offset 0 by successive msnprintf calls. Every byte up to the final `len` is initialized by a write. varval is initialized to empty string and only ever holds values parsed from attacker-controlled input or retains a previous loop iteration's value (also attacker-controlled). No path exists for uninitialized stack memory to be copied into the output buffer or sent on the wire.
Running curl with crafted NEW_ENV environment variables that cause sscanf to parse only one field results in uninitialized data being written to the output buffer
Read the reasoning
The reproduction demonstrates the logic bug (wrong varval used), but the observed non-'a' bytes in the packet capture are not explained by the provided code; the code shows no mechanism for stack content disclosure.
Read the original report (excerpt)
Source code
| 876 | static void suboption(struct Curl_easy *data) |
| C2: The suboption function is defined at line 876 and contains the vulnerable logic at line 924. | |
| 877 | { |
| 878 | struct curl_slist *v; |
| 879 | unsigned char temp[2048]; |
| 880 | ssize_t bytes_written; |
| 881 | size_t len; |
| 882 | int err; |
| 883 | char varname[128] = ""; |
| C4: `char varval[128] = "";` initializes varval to empty string at function entry. | |
| 884 | char varval[128] = ""; |
| C4: `char varname[128] = "";` similarly initialized. | |
| 885 | struct TELNET *tn = data->req.p.telnet; |
| 886 | struct connectdata *conn = data->conn; |
| 887 | |
| 888 | printsub(data, '<', (unsigned char *)tn->subbuffer, CURL_SB_LEN(tn) + 2); |
| 889 | switch(CURL_SB_GET(tn)) { |
| 890 | case CURL_TELOPT_TTYPE: |
| 891 | len = strlen(tn->subopt_ttype) + 4 + 2; |
| 892 | msnprintf((char *)temp, sizeof(temp), |
| 893 | "%c%c%c%c%s%c%c", CURL_IAC, CURL_SB, CURL_TELOPT_TTYPE, |
| 894 | CURL_TELQUAL_IS, tn->subopt_ttype, CURL_IAC, CURL_SE); |
| 895 | bytes_written = swrite(conn->sock[FIRSTSOCKET], temp, len); |
| 896 | if(bytes_written < 0) { |
| 897 | err = SOCKERRNO; |
| 898 | failf(data,"Sending data failed (%d)",err); |
| 899 | } |
| 900 | printsub(data, '>', &temp[2], len-2); |
| 901 | break; |
| 902 | case CURL_TELOPT_XDISPLOC: |
| 903 | len = strlen(tn->subopt_xdisploc) + 4 + 2; |
| 904 | msnprintf((char *)temp, sizeof(temp), |
| 905 | "%c%c%c%c%s%c%c", CURL_IAC, CURL_SB, CURL_TELOPT_XDISPLOC, |
| 906 | CURL_TELQUAL_IS, tn->subopt_xdisploc, CURL_IAC, CURL_SE); |
| 907 | bytes_written = swrite(conn->sock[FIRSTSOCKET], temp, len); |
| 908 | if(bytes_written < 0) { |
| 909 | err = SOCKERRNO; |
| 910 | failf(data,"Sending data failed (%d)",err); |
| 911 | } |
| 912 | printsub(data, '>', &temp[2], len-2); |
| 913 | break; |
| 914 | case CURL_TELOPT_NEW_ENVIRON: |
| 915 | msnprintf((char *)temp, sizeof(temp), |
| 916 | "%c%c%c%c", CURL_IAC, CURL_SB, CURL_TELOPT_NEW_ENVIRON, |
| 917 | CURL_TELQUAL_IS); |
| 918 | len = 4; |
| 919 | |
| 920 | for(v = tn->telnet_vars; v; v = v->next) { |
| C3: The loop iterates over `tn->telnet_vars` which is populated from CURLOPT_TELNETOPTIONS, controlled by the caller. | |
| 921 | size_t tmplen = (strlen(v->data) + 1); |
| 922 | /* Add the variable only if it fits */ |
| 923 | if(len + tmplen < (int)sizeof(temp)-6) { |
| 924 | if(sscanf(v->data, "%127[^,],%127s", varname, varval)) { |
| C1: The code checks `if(sscanf(v->data, "%127[^,],%127s", varname, varval))` which evaluates to true for any non-zero return (1 or 2), not strictly 2. | |
| C5: The sscanf check passes for single-field input, causing the branch to execute with varval unchanged. | |
| 925 | msnprintf((char *)&temp[len], sizeof(temp) - len, |
| C4: msnprintf writes to `&temp[len]` sequentially from the start of the buffer. | |
| 926 | "%c%s%c%s", CURL_NEW_ENV_VAR, varname, |
| 927 | CURL_NEW_ENV_VALUE, varval); |
| 928 | len += tmplen; |
| 929 | } |
| 930 | } |
| 931 | } |
| 932 | msnprintf((char *)&temp[len], sizeof(temp) - len, |
| 933 | "%c%c", CURL_IAC, CURL_SE); |
| 934 | len += 2; |
| 935 | bytes_written = swrite(conn->sock[FIRSTSOCKET], temp, len); |
| C4: swrite sends exactly `len` bytes from the beginning of temp. | |
| 936 | if(bytes_written < 0) { |
| 937 | err = SOCKERRNO; |
| 938 | failf(data,"Sending data failed (%d)",err); |
| 939 | } |
| 940 | printsub(data, '>', &temp[2], len-2); |
| 941 | break; |
| 942 | } |
| 943 | return; |
| 944 | } |