Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

windows: clear sensitive command-line options from process list #15620

Closed
wants to merge 25 commits into from

Conversation

vszakats
Copy link
Member

@vszakats vszakats commented Nov 21, 2024

Writing to argv is not effective on Windows, but there is an alternate
method to modify the command-line as seen here:
https://github.com/lordmulder/cURL-build-win32/blob/2e9d3818baf8f7b1183331750b7b33152dd6ee82/patch/curl_tool_doswin.diff

Try a patch that's re-creating the full command-line string from each
argument except sensitive ones and writing it back to the command-line
buffer.

Screen Shot 2024-11-21 non-unicode
Screen Shot 2024-11-21 unicode

The known issue is the use of banned func _tcscat().


@vszakats vszakats added cmdline tool Windows Windows-specific labels Nov 21, 2024
@vszakats vszakats marked this pull request as draft November 21, 2024 01:28
@vszakats vszakats changed the title windows: clear sensitive cmdline options from process list windows: clear sensitive cmd-line options from process list Nov 21, 2024
@vszakats vszakats changed the title windows: clear sensitive cmd-line options from process list windows: clear sensitive command-line options from process list Nov 21, 2024
@vszakats vszakats force-pushed the w-writable-args branch 2 times, most recently from 2f74a72 to 2046ba9 Compare November 21, 2024 01:58
@jay
Copy link
Member

jay commented Nov 21, 2024

I don't like this, I think it's unnecessary and adds complexity. Windows users can't see other users' command lines without privilege, unlike other operating systems. I use Process Explorer a lot and find the command lines useful to see what's running. I can't think of another application on my system which modifies command lines like this.

Ref: #10888 (comment)

@vszakats
Copy link
Member Author

vszakats commented Nov 21, 2024

Calling this malware doesn't help. There is nothing malicious in hiding secrets from public buffers.

Having secrets visible for other processes running under the same or any higher privileged accounts is a legit concern I think. Is it common that someone wants to recover (or see) their own credentials via Process Explorer? All the rest of the command-line is still visible after this patch. (Contrary to the linked patch which hides it entirely.)

I agree it's not a beauty but that's not unlike other Windows-specific quirks already rolled within the source. It probably can be tidied-up by moving the complexity inside tool_doswin.

@vszakats vszakats force-pushed the w-writable-args branch 3 times, most recently from 6cf3827 to 7c2cb2b Compare November 21, 2024 11:05
@vszakats
Copy link
Member Author

vszakats commented Nov 21, 2024

The HTTP/3 wolfSSL CMake failure is unrelated and possibly caused by a recent upstream commit (between be70bea687526a51e3d751d425bbaaa412b451ee and c06f65a8ace311667d9b9d7fd320b6b25f8b1bf8: https://github.com/wolfSSL/wolfSSL/compare/be70bea687526a51e3d751d425bbaaa412b451ee..c06f65a8ace311667d9b9d7fd320b6b25f8b1bf8) or the image update (from 20241112.1.0 to 20241117.1.0), after which these two features are no longer detected in curl CMake builds:
HAVE_WOLFSSL_BIO, HAVE_WOLFSSL_FULL_BIO. Then building without HTTPS-proxy and failing the corresponding pytests.

/home/runner/work/curl/curl/CMakeFiles/CMakeScratch//CheckSymbolExists.c:8:19: error: ‘wolfSSL_BIO_new’ undeclared (first use in this function); did you mean ‘wolfSSL_CTX_new’?
/home/runner/work/curl/curl/CMakeFiles/CMakeScratch//CheckSymbolExists.c:8:19: error: ‘wolfSSL_BIO_set_shutdown’ undeclared (first use in this function); did you mean ‘wolfSSL_set_shutdown’?

@jay
Copy link
Member

jay commented Nov 21, 2024

To be fair, in the other thread I said modifying the command line is something malware does, and that isn't saying your code is malware.

If a user puts secrets in command lines then I don't think it's the responsibility of any program to have to remedy that. Also, I was opposed to this behavior in general for all operating systems in part because it's not completely effective. As long as operating systems allow it adversaries can poll command lines for secrets.

vszakats added a commit that referenced this pull request Nov 21, 2024
It was missing while detecting `wolfSSL_DES_ecb_encrypt`,
`wolfSSL_BIO_new` and `wolfSSL_BIO_set_shutdown`.

We have not seen it causing issues in stable wolfSSL releases as of
v5.7.4, until a recent commit in wolfSSL master, which broke detections:
```
curl/CMakeFiles/CMakeScratch//CheckSymbolExists.c:8:19: error: ‘wolfSSL_BIO_new’ undeclared (first use in this function); did you mean ‘wolfSSL_CTX_new’?
curl/CMakeFiles/CMakeScratch//CheckSymbolExists.c:8:19: error: ‘wolfSSL_BIO_set_shutdown’ undeclared (first use in this function); did you mean ‘wolfSSL_set_shutdown’?
```
This in turn disabled `HTTPS-proxy` and failed related pytests:
https://github.com/curl/curl/actions/runs/11953800545/job/33324250039?pr=15620

wolfSSL source diff causing the regression:
https://github.com/wolfSSL/wolfSSL/compare/be70bea687526a51e3d751d425bbaaa412b451ee..c06f65a8ace311667d9b9d7fd320b6b25f8b1bf8

The wolfSSL build says:
```
Note: Make sure your application includes "wolfssl/options.h" before any other wolfSSL headers.
      You can define "WOLFSSL_USE_OPTIONS_H" in your application to include this automatically.
```

This patch makes sure to follow this rule across the curl codebase.

Also:
- include `wolfssl/options.h` first in `lib/vtls/wolfssl.c`.
  It was preceded by `wolfssl/version.h`, which did not cause issues.
  Background for the pre-existing include order:
  Ref: deb9462 #3903
  Ref: https://curl.se/mail/lib-2015-04/0069.html

Bug: #15620 (comment)
Follow-up to d68a121 #14064

Closes #15623
@vszakats
Copy link
Member Author

vszakats commented Nov 21, 2024

To be fair, in the other thread I said modifying the command line is something malware does, and that isn't saying your code is malware.

Thanks, no hard feelings!

If a user puts secrets in command lines then I don't think it's the responsibility of any program to have to remedy that. Also, I was opposed to this behavior in general for all operating systems in part because it's not completely effective. As long as operating systems allow it adversaries can poll command lines for secrets.

Fair point. This is indeed not a 100% solution, but rather something that helps
in likely many practical cases.

A while ago spent an unjustifiable amount of time converting all scripts to
not pass secrets via the command-line. It was complicated, non-obvious,
full of gotchas, even tool bugs, and after several iterations, still not a 100%
solution. Later got a message that my method is in fact broken, or may be
broken, depending. This was in POSIX/bash shells, and I imagine doing the
same with batches is possibly worse. I don't know about PowerShell.

I think making the common use-case safer, may help this. While also making
it clear that neither this or the write-arg solution replaces the superior methods.

All that said, I'm curious what is the safe way to pass secrets to a command-line
tool without exposing it to the command-line, envs, saving it to disk, etc.
Apparently pipes are also not safe, and even a printf can be an external
process in some environments.

edit: managed to reduce this patch today, so now it's below 60 lines of extra code,
down from ~100.

@github-actions github-actions bot added the CI Continuous Integration label Nov 21, 2024
@jay
Copy link
Member

jay commented Nov 21, 2024

curl -K - << 'EOF'
  verbose
  anyauth
  noproxy *
  user root:thisisthepassword
  url 192.168.1.80:8000/axis-cgi/pwdgrp.cgi
  data action=add&user=operator&pwd=alsoapassword&grp=operator
EOF

Note in this example (which I stole from one of my mailing list posts) the 'EOF' is quoted to prevent variable expansion, sometimes people allow that though. Also note if you do it at the prompt the whole thing will likely go in .bash_history

@vszakats
Copy link
Member Author

curl -K - << 'EOF'
  verbose
  anyauth
  noproxy *
  user root:thisisthepassword
  url 192.168.1.80:8000/axis-cgi/pwdgrp.cgi
  data action=add&user=operator&pwd=alsoapassword&grp=operator
EOF

Note in this example (which I stole from one of my mailing list posts) the 'EOF' is quoted to prevent variable expansion, sometimes people allow that though. Also note if you do it at the prompt the whole thing will likely go in .bash_history

I didn't know about the EOF quoting trick! Though often the secret
comes from somewhere dynamically, e.g. from /usr/bin/security on
macOS, and expansion is desired in such cases. Such redirection is what
I ended up with. Unless there are multiple secrets to pass, in which case
there is the exec 3<<EOF trick for multiple pipes. I forgot which of
these were told to be unsafe, because the pipe appear as a file (?) or
as a /dev/ device somewhere (?). (with curl it's possible to redirect a config
with multiple secrets in them, helping with this.)

One way to avoid redirections is by the app accepting a command, which
it calls to retrieve the secret. That command can be whatever secure
secret retriever method the system provides (e.g. /usr/bin/security or
some secure agent). ssh supports it via SSH_ASKPASS. Also
backup software like Restic and Borg.

skip the feature entirely if either of _acmdln or _wcmdln is NULL.
skip clearing cmdlin buffers if the ANSI-WIDE conversion failed.
```
D:\a\curl\curl\src\tool_getparam.c(2819,1): warning C4701: potentially uninitialized local variable 'acmdln_siz' used [D:\a\curl\curl\bld\src\curl.vcxproj]
D:\a\curl\curl\src\tool_getparam.c(2818,1): warning C4701: potentially uninitialized local variable 'wcmdln_siz' used [D:\a\curl\curl\bld\src\curl.vcxproj]
```
https://github.com/curl/curl/actions/runs/11953256088/job/33321005862?pr=15620#step:9:36

```
D:/a/curl/curl/src/tool_getparam.c: In function 'parse_args':
D:/a/curl/curl/src/tool_getparam.c:2819:11: error: 'acmdln_siz' may be used uninitialized [-Werror=maybe-uninitialized]
 2819 |     (void)WideCharToMultiByte(CP_ACP, 0, tcmdln, -1,
      |           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 2820 |                               _acmdln, (int)acmdln_siz, NULL, NULL);
      |                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D:/a/curl/curl/src/tool_getparam.c:2695:22: note: 'acmdln_siz' was declared here
 2695 |   size_t acmdln_len, acmdln_siz;
      |                      ^~~~~~~~~~
D:/a/curl/curl/src/tool_getparam.c:2818:5: error: 'wcmdln_siz' may be used uninitialized [-Werror=maybe-uninitialized]
 2818 |     memcpy(_wcmdln, tcmdln, wcmdln_siz);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D:/a/curl/curl/src/tool_getparam.c:2696:22: note: 'wcmdln_siz' was declared here
 2696 |   size_t wcmdln_len, wcmdln_siz;
      |                      ^~~~~~~~~~
```
https://github.com/curl/curl/actions/runs/11953256088/job/33321008571?pr=15620#step:19:23
@vszakats vszakats marked this pull request as ready for review November 22, 2024 03:15
@jay
Copy link
Member

jay commented Nov 22, 2024

This is rebuilding the command line in a way that may not accurately represent what it was, aside from the intentional omission of secrets. How and which command line (a&w) is used and how it is parsed varies. I think we may have discussed this at some point in some other issue. Windows doesn't have standard argument parsing and it doesn't give the app the arguments, it just gives it the command line and expects the app to parse it however it chooses.

There's no guarantee that acmd and wcmd will point to the process' internal command line that is used by Windows. Also you are assuming acmdln and wcmdln will both exist and be less than the sum of all the parsed argv plus spaces, which makes sense but afaict isn't guaranteed.

src/tool_getparam.c Outdated Show resolved Hide resolved
@bagder
Copy link
Member

bagder commented Nov 23, 2024

We also have the variable system that is another way to get data into a command line without being visible in the process listing.

I think this solution unfortunately is very complicated, feels brittle and hard to work with due to its completely different nature than the "normal" argument blanking (which value has always been low). This makes me side with @jay here: this implementation does not seem to outweigh the cost of accepting it for the small gain we get with it.

@vszakats
Copy link
Member Author

vszakats commented Nov 23, 2024

The UCRT source code says these pointers are initialized with
GetCommandLineW() and GetCommandLineA() before calling
main(). It says they may be NULL. (an earlier iteration of this PR
handled any NULL combinations.)

https://github.com/huangqinjin/ucrt/blob/d6e817a4cc90f6f1fe54f8a0aa4af4fff0bb647d/startup/argv_data.cpp#L63-L64

But, the MSDN documentation says that the buffer returned by
these functions should not be modified, which makes this indeed
brittle, and a strong enough reason to abandon this:
https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getcommandlinew

Return value
The return value is a pointer to the command-line string for the current process.

Remarks
The lifetime of the returned value is managed by the system, applications should not free or modify this value.

Thanks for the reviews and remarks! Closing this without a merge.

@vszakats vszakats closed this Nov 23, 2024
@vszakats vszakats deleted the w-writable-args branch November 23, 2024 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI Continuous Integration cmdline tool Windows Windows-specific
Development

Successfully merging this pull request may close these issues.

3 participants