CVE-2022-35260: .netrc parser out-of-bounds access
What the report claims
Curl expects the .netrc file to have space characters; if it lacks spaces, parsenetrc() performs out-of-bounds reads and a 1-byte out-of-bounds write, potentially causing crashes or other consequences.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2022-35260 .netrc parser out-of-bounds access
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 while(!ISSPACE(*tok_end)) loop at lib/netrc.c:111 lacks a bounds check against the netrcbuffer end.
- fgets guarantees null-termination within the 4096-byte buffer, but ISSPACE('\0') is false, so the loop continues past the buffer.
- The subsequent *tok_end = 0 at line 113 writes a null byte out-of-bounds.
- An attacker controlling the .netrc file content (via --netrc-file) can trigger this reliably with a file containing no whitespace characters.
How this analysis ran 5 claims · 9 citations verified
Checked against curl release curl-7_85_0 (fits the report's line numbers and quotes better than master on 2022-10-03) (93d092867f0f). Test 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 report. The vulnerability is confirmed: in lib/netrc.c, the loop at line 111 (while(!ISSPACE(*tok_end)) tok_end++;) lacks a bounds check, allowing tok_end to run past the 4096-byte netrcbuffer when the .netrc file contains no whitespace. The write at line 113 (*tok_end = 0;) then writes a null byte out-of-bounds. A 4095-byte file of 'a' with no newline reliably triggers this. We will prepare a fix adding a buffer-end check to the loop condition.
Inspect all claims and source code 5 claims · 5 code excerpts
Report claims
parsenetrc() performs an out-of-bounds read when encountering non-space characters in the .netrc file.
Read the reasoning
The loop at line 111 increments tok_end while the character is not a space. With a .netrc file containing 4095 'a' bytes and no newline, fgets fills netrcbuffer[0..4094] with 'a' and places '\0' at netrcbuffer[4095]. Since ISSPACE('\0') is false (Curl_isspace(0) returns 0), the loop reads netrcbuffer[4095] (valid), increments tok_end to 4096, then reads netrcbuffer[4096] (out-of-bounds) for the next iteration's condition check.
parsenetrc() performs a 1-byte out-of-bounds write at line 27.
Read the reasoning
After the while(!ISSPACE(*tok_end)) loop exits (when it randomly encounters a byte that ISSPACE considers a space), line 113 writes a null terminator at that out-of-bounds tok_end position. This is a 1-byte out-of-bounds write.
A .netrc file consisting of 4095 bytes of the character 'a' (no space or newline) can trigger the flaw.
Read the reasoning
A 4095-byte file of 'a' with no newline causes fgets to fill the entire buffer (indices 0-4094 = 'a', index 4095 = '\0'). Since 'a' and '\0' are both non-space, the tok_end loop runs past the buffer end. Smaller files can also trigger it if memory after the buffer lacks space characters.
Application crash plus other as yet undetermined consequences.
Read the reasoning
Buffer overrun reads and writes are undefined behavior in C and can cause application crashes, memory corruption, or potential code execution depending on heap layout.
Running `curl --netrc-file .netrc test.local` with a .netrc file of 4095 'a' bytes reproduces the issue.
Read the reasoning
The --netrc-file command line option triggers parsenetrc() with the attacker-controlled .netrc file, making the reproduction steps accurate.
Read the original report
Source code
| 58 | static int parsenetrc(const char *host, |
| C5: parsenetrc is called when --netrc-file option is used | |
| 59 | char **loginp, |
| 60 | char **passwordp, |
| 61 | bool *login_changed, |
| 62 | bool *password_changed, |
| 63 | char *netrcfile) |
| 64 | { |
| 65 | FILE *file; |
| 66 | int retcode = NETRC_FILE_MISSING; |
| 67 | char *login = *loginp; |
| 68 | char *password = *passwordp; |
| 69 | bool specific_login = (login && *login != 0); |
| 70 | bool login_alloc = FALSE; |
| 71 | bool password_alloc = FALSE; |
| 72 | enum host_lookup_state state = NOTHING; |
| 73 | |
| 74 | char state_login = 0; /* Found a login keyword */ |
| 75 | char state_password = 0; /* Found a password keyword */ |
| 76 | int state_our_login = TRUE; /* With specific_login, found *our* login |
| 77 | name (or login-less line) */ |
| 78 | |
| 79 | DEBUGASSERT(netrcfile); |
| 80 | |
| 81 | file = fopen(netrcfile, FOPEN_READTEXT); |
| 82 | if(file) { |
| 83 | bool done = FALSE; |
| 84 | char netrcbuffer[4096]; |
| C3: char netrcbuffer[4096]; buffer size is 4096 bytes | |
| 85 | int netrcbuffsize = (int)sizeof(netrcbuffer); |
| C3: int netrcbuffsize = (int)sizeof(netrcbuffsize); passed to fgets | |
| 86 | |
| 87 | while(!done && fgets(netrcbuffer, netrcbuffsize, file)) { |
| C3: fgets(netrcbuffer, netrcbuffsize, file) reads at most 4095 chars + null terminator | |
| 88 | char *tok; |
| 89 | char *tok_end; |
| 90 | bool quoted; |
| 91 | if(state == MACDEF) { |
| 92 | if((netrcbuffer[0] == '\n') || (netrcbuffer[0] == '\r')) |
| 93 | state = NOTHING; |
| 94 | else |
| 95 | continue; |
| 96 | } |
| 97 | tok = netrcbuffer; |
| 98 | while(tok) { |
| 99 | while(ISSPACE(*tok)) |
| 100 | tok++; |
| 101 | /* tok is first non-space letter */ |
| 102 | if(!*tok || (*tok == '#')) |
| 103 | /* end of line or the rest is a comment */ |
| 104 | break; |
| 105 | |
| 106 | /* leading double-quote means quoted string */ |
| 107 | quoted = (*tok == '\"'); |
| 108 | |
| 109 | tok_end = tok; |
| 110 | if(!quoted) { |
| 111 | while(!ISSPACE(*tok_end)) |
| C1: while(!ISSPACE(*tok_end)) tok_end++; reads past the buffer when no space character exists in the netrcbuffer | |
| C4: Out-of-bounds read in loop condition | |
| 112 | tok_end++; |
| C1: tok_end increments without bounds check; fgets guarantees null-termination at netrcbuffer[4095] but ISSPACE('\0') is false, so loop continues past buffer end | |
| 113 | *tok_end = 0; |
| C2: *tok_end = 0; writes a null byte at the position where the loop stopped, which can be one or more bytes past the 4096-byte buffer | |
| C4: Out-of-bounds write of null byte | |
| 114 | } |
| 115 | else { |
| 116 | bool escape = FALSE; |
| 117 | bool endquote = FALSE; |
| 118 | char *store = tok; |
| 119 | tok_end++; /* pass the leading quote */ |
| 120 | while(*tok_end) { |
| 121 | char s = *tok_end; |
| 122 | if(escape) { |
| 123 | escape = FALSE; |
| 124 | switch(s) { |
| 125 | case 'n': |
| 126 | s = '\n'; |
| 127 | break; |
| 128 | case 'r': |
| 129 | s = '\r'; |
| 130 | break; |
| 131 | case 't': |
| 132 | s = '\t'; |
| 133 | break; |
| 134 | } |
| 135 | } |
| 136 | else if(s == '\\') { |
| 137 | escape = TRUE; |
| 138 | tok_end++; |
| 139 | continue; |
| 140 | } |
| 141 | else if(s == '\"') { |
| 142 | tok_end++; /* pass the ending quote */ |
| 143 | endquote = TRUE; |
| 144 | break; |
| 145 | } |
| 146 | *store++ = s; |
| 147 | tok_end++; |
| 148 | } |
| 149 | *store = 0; |
| 150 | if(escape || !endquote) { |
| 151 | /* bad syntax, get out */ |
| 152 | retcode = NETRC_FAILED; |
| 153 | goto out; |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | if((login && *login) && (password && *password)) { |
| 158 | done = TRUE; |
| 159 | break; |
| 160 | } |
| 161 | |
| 162 | switch(state) { |
| 163 | case NOTHING: |
| 164 | if(strcasecompare("macdef", tok)) { |
| 165 | /* Define a macro. A macro is defined with the specified name; its |
| 166 | contents begin with the next .netrc line and continue until a |
| 167 | null line (consecutive new-line characters) is encountered. */ |
| 168 | state = MACDEF; |
| 169 | } |
| 170 | else if(strcasecompare("machine", tok)) { |
| 171 | /* the next tok is the machine name, this is in itself the |
| 172 | delimiter that starts the stuff entered for this machine, |
| 173 | after this we need to search for 'login' and |
| 174 | 'password'. */ |
| 175 | state = HOSTFOUND; |
| 176 | } |
| 177 | else if(strcasecompare("default", tok)) { |
| 178 | state = HOSTVALID; |
| 179 | retcode = NETRC_SUCCESS; /* we did find our host */ |
| 180 | } |
| 181 | break; |
| 182 | case MACDEF: |
| 183 | if(!strlen(tok)) { |
| 184 | state = NOTHING; |
| 185 | } |
| 186 | break; |
| 187 | case HOSTFOUND: |
| 188 | if(strcasecompare(host, tok)) { |
| 189 | /* and yes, this is our host! */ |
| 190 | state = HOSTVALID; |
| 191 | retcode = NETRC_SUCCESS; /* we did find our host */ |
| 192 | } |
| 193 | else |
| 194 | /* not our host */ |
| 195 | state = NOTHING; |
| 196 | break; |
| 197 | case HOSTVALID: |
| 198 | /* we are now parsing sub-keywords concerning "our" host */ |
| 199 | if(state_login) { |
| 200 | if(specific_login) { |
| 201 | state_our_login = strcasecompare(login, tok); |
| 202 | } |
| 203 | else if(!login || strcmp(login, tok)) { |
| 204 | if(login_alloc) { |
| 205 | free(login); |
| 206 | login_alloc = FALSE; |
| 207 | } |
| 208 | login = strdup(tok); |
| 209 | if(!login) { |
| 210 | retcode = NETRC_FAILED; /* allocation failed */ |
| 211 | goto out; |
| 212 | } |
| 213 | login_alloc = TRUE; |
| 214 | } |
| 215 | state_login = 0; |
| 216 | } |
| 217 | else if(state_password) { |
| 218 | if((state_our_login || !specific_login) |
| 80 | # define read(fd, buf, count) curlx_read(fd, buf, count) |
Show 1 lines (not cited by the judge)
| 40 | #define ISSPACE(x) (Curl_isspace((int) ((unsigned char)x))) |
Show 1 lines (not cited by the judge)
| 242 | # define true 1 |
Show 44 lines (not cited by the judge)
| 40 | while(<STDIN>) { |
| 41 | if(/^ *CURLOPT\(([^,]*), ([^,]*), (\d+)\)/) { |
| 42 | my($opt, $type, $num)=($1,$2,$3); |
| 43 | my $name; |
| 44 | my $ext = $type; |
| 45 | |
| 46 | if($opt =~ /OBSOLETE/) { |
| 47 | # skip obsolete options |
| 48 | next; |
| 49 | } |
| 50 | |
| 51 | if($opt =~ /^CURLOPT_(.*)/) { |
| 52 | $name=$1; |
| 53 | } |
| 54 | $ext =~ s/CURLOPTTYPE_//; |
| 55 | $ext =~ s/CBPOINT/CBPTR/; |
| 56 | $ext =~ s/POINT\z//; |
| 57 | $type = "CURLOT_$ext"; |
| 58 | |
| 59 | $opt{$name} = $opt; |
| 60 | $type{$name} = $type; |
| 61 | push @names, $name; |
| 62 | if($num < $lastnum) { |
| 63 | print STDERR "ERROR: $opt has bad number\n"; |
| 64 | exit 2; |
| 65 | } |
| 66 | else { |
| 67 | $lastnum = $num; |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | # alias for an older option |
| 72 | # old = new |
| 73 | if(/^#define (CURLOPT_[^ ]*) *(CURLOPT_\S*)/) { |
| 74 | my ($o, $n)=($1, $2); |
| 75 | # skip obsolete ones |
| 76 | if($n !~ /OBSOLETE/) { |
| 77 | $o =~ s/^CURLOPT_//; |
| 78 | $n =~ s/^CURLOPT_//; |
| 79 | $alias{$o} = $n; |
| 80 | push @names, $o, |
| 81 | } |
| 82 | } |
| 83 | } |