CVE-2023-27534: SFTP path ~ resolving discrepancy
What the report claims
libcurl's Curl_getworkingpath function incorrectly resolves '~' in SFTP paths, converting '/~/' to the remote user's home directory while not handling '/~/' or '~<something>' correctly, potentially allowing path traversal bypasses.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2023-27534 SFTP path ~ resolving discrepancy
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?
- SFTP path handling at lib/curl_path.c:64 accepts any '/~...' prefix, not just '/~/' or '/~', unlike the SCP handling at line 57 and the documented behavior.
- This allows '/~a..' to be transformed into '/home/user/../other/file', creating a path-traversal primitive that can bypass application-level '..' filtering.
- The flaw is reachable via attacker-controlled SFTP URL path and gives the attacker access to parent directories of the home directory.
How this analysis ran 5 claims · 7 citations verified
Checked against curl master on 2023-03-05 (e135bc9d31ad). 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 report. The code in lib/curl_path.c confirms the discrepancy: the SFTP branch (line 64) treats any path starting with '/~' as home-relative, while the SCP branch (line 57) and the documentation only specify '/~/' or '/~'. This allows '/~a..' to become '/home/user/../other/file', bypassing '..' filters. We will treat this as a valid security issue and work on a fix that restricts SFTP tilde expansion to '/~/' and '/~' only.
Inspect all claims and source code 5 claims · 1 code excerpts
Report claims
Curl_getworkingpath resolves '~' in SFTP paths to remote users' home directory in an undocumented way
Read the reasoning
The SFTP code at line 64 uses a broad check (second character is '~') that matches '/~a..', '/~user', etc., while the comment at line 50 and the SCP implementation suggest the intent was to handle only '/~/' or '/~'. This discrepancy is the undocumented behavior.
The function is said to convert '/~/' to remote user's home directory
Read the reasoning
The report cites https://everything.curl.dev/usingcurl/scpsftp as stating '/~/' is converted to the home directory. The provided external sources (E1, E2) are unrelated pages and do not contain the curl documentation. Without the actual documentation excerpt, this claim about what the docs say cannot be verified from the given material.
Accessing 'sftp://host/~a../other/file' triggers the path resolution
Read the reasoning
The URL path 'sftp://host/~a../other/file' becomes working_path '/~a../other/file' after urldecode. The second character is '~', so the SFTP branch at line 64 executes.
Allows an attacker with partial path access to gain access to untended remote system path locations
Read the reasoning
For '/~a../other/file', working_path+3 points to '..', so the result is '/home/user/../other/file'. An application that filters '..' path components but allows '~' prefixes would be bypassed, because '~a..' is a single path component that does not contain '..' until after curl's substitution. This is a plausible path-traversal vector.
Remote path results as '/home/user/../other/file' when using '~a..' component
Read the reasoning
The concatenation yields '/home/user/' + '../other/file' = '/home/user/../other/file', exactly as the report states.
What the report says about other projects
not addressed The curl documentation states that '~' in SFTP paths is resolved to the remote user's home directory only when used as '/~/' or '/~' (full path).
Read the original report
Source code
| 36 | CURLcode Curl_getworkingpath(struct Curl_easy *data, |
| 37 | char *homedir, /* when SFTP is used */ |
| 38 | char **path) /* returns the allocated |
| 39 | real path to work with */ |
| 40 | { |
| 41 | char *real_path = NULL; |
| 42 | char *working_path; |
| 43 | size_t working_path_len; |
| 44 | CURLcode result = |
| 45 | Curl_urldecode(data->state.up.path, 0, &working_path, |
| 46 | &working_path_len, REJECT_ZERO); |
| 47 | if(result) |
| 48 | return result; |
| 49 | |
| 50 | /* Check for /~/, indicating relative to the user's home directory */ |
| 51 | if(data->conn->handler->protocol & CURLPROTO_SCP) { |
| 52 | real_path = malloc(working_path_len + 1); |
| 53 | if(!real_path) { |
| 54 | free(working_path); |
| 55 | return CURLE_OUT_OF_MEMORY; |
| 56 | } |
| 57 | if((working_path_len > 3) && (!memcmp(working_path, "/~/", 3))) |
| C1: SCP branch by contrast checks specifically for '/~/' via memcmp(working_path, "/~/", 3) | |
| 58 | /* It is referenced to the home directory, so strip the leading '/~/' */ |
| 59 | memcpy(real_path, working_path + 3, working_path_len - 2); |
| 60 | else |
| 61 | memcpy(real_path, working_path, 1 + working_path_len); |
| 62 | } |
| 63 | else if(data->conn->handler->protocol & CURLPROTO_SFTP) { |
| 64 | if((working_path_len > 1) && (working_path[1] == '~')) { |
| C1: SFTP branch triggers on any path where working_path[1] == '~' (i.e., '/~...'), not only '/~/' or '/~' | |
| C3: Condition (working_path_len > 1) && (working_path[1] == '~') is true for '/~a../other/file' (second char is '~') | |
| 65 | size_t homelen = strlen(homedir); |
| 66 | real_path = malloc(homelen + working_path_len + 1); |
| 67 | if(!real_path) { |
| 68 | free(working_path); |
| 69 | return CURLE_OUT_OF_MEMORY; |
| 70 | } |
| 71 | /* It is referenced to the home directory, so strip the |
| C4: Code copies homedir (e.g., '/home/user'), adds '/', then appends working_path+3 | |
| 72 | leading '/' */ |
| 73 | memcpy(real_path, homedir, homelen); |
| C5: Copies homedir (e.g., '/home/user') then adds '/' at lines 75-78 | |
| 74 | /* Only add a trailing '/' if homedir does not end with one */ |
| 75 | if(homelen == 0 || real_path[homelen - 1] != '/') { |
| 76 | real_path[homelen] = '/'; |
| 77 | homelen++; |
| 78 | real_path[homelen] = '\0'; |
| 79 | } |
| 80 | if(working_path_len > 3) { |
| 81 | memcpy(real_path + homelen, working_path + 3, |
| C4: memcpy(real_path + homelen, working_path + 3, ...) copies from the 4th character onward | |
| C5: Copies from working_path+3; for '/~a../other/file' that is '../other/file' | |
| 82 | 1 + working_path_len -3); |
| 83 | } |
| 84 | } |
| 85 | else { |
| 86 | real_path = malloc(working_path_len + 1); |
| 87 | if(!real_path) { |
| 88 | free(working_path); |
| 89 | return CURLE_OUT_OF_MEMORY; |
| 90 | } |
| 91 | memcpy(real_path, working_path, 1 + working_path_len); |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | free(working_path); |
| 96 | |
| 97 | /* store the pointer for the caller to receive */ |
| 98 | *path = real_path; |
| 99 | |
| 100 | return CURLE_OK; |
| 101 | } |