✓ProveIt
Examples / Report #1176461 Saved analysis

CVE-2021-22898: TELNET stack contents disclosure

curl · 2021-04-2727.4s analysis · $0.033 in model calls

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

Needs human review

The available evidence does not settle the report. A maintainer should investigate.

Judgment withdrawn. The answer failed the evidence check.
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.

1 · Grounding
grounded
no model · 6 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 9372 tokens · $0.0007
3 · Evidence
1 code snippets
real curl source at 2e23f3b8d54c · 1 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
18699 tokens · 24.8s · $0.0319
5 · Receipts
8 of 8 citations verified
every citation must point at code the judge was shown
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

C1flaw supported

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.

C2location supported

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.

C3trigger supported

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.

C4impact contradicted

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.

C5reproduction supported

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)

Full report on HackerOne ↗

## Summary: lib/telnet.c `suboption` function incorrecly checks for the `sscanf` return value. Instead of checking that 2 elements are parsed, the code also continues if just one element matches: `if(sscanf(v->data, "%127[^,],%127s", varname, varval)) {` As such it is possible to construct environment values that don't update the `varval` buffer and instead use the previous value. In combination of advancing in the `temp` buffer by `strlen(v->data) + 1`, this means that there will be uninitialized gaps in the generated output `temp` buffer. These gaps will contain whatever stack contents from previous operation of the application. Fortunately the environment is controlled by the client and not the server. As such this vulnerability can't be exploited by the server. Practical exploitation is limited by the following requirements: - attacker is able to control the environment passed to libcurl via `CURLOPT_TELNETOPTIONS` ("`NEW_ENV=xxx,yyy`") and control `xxx` and `yyy` in the curl_slist entries) - attacker is able to either inspect the network traffic of the telnet connection or to select the server/port the connection is established to When both are true the attacker is able to some content of the stack. Note however that for this leak to be meaningful, some confidential or sensitive information would need to be leaked. This could happen if some key or other sensitive material (that is otherwise out of the reach of the attacker, due to for example setuid + dropping of privileges, or for example only being able to execute the command remotely in a limited fashion, for example php curl, or similar) would thus become visible fully, or partially. The leak is limited to maximum about half of the 2048 byte `temp` buffer. ## Steps To Reproduce: 1. Run telnet service 2. tcpdump -i lo -X -s 65535 port 23 2. Execute ``` curl -tNEW_ENV=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -tNEW_ENV=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -tNEW …

Source code

lib/telnet.clines 876–944 · definition of suboption
876static 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}