33

This question about using cURL with a username and password has suboptimal answers for me:

  1. curl -u "user:pw" https://example.com puts the pw in the process list
  2. curl "https://user:pw@example.com" puts the pw in the process list
  3. curl -u "user:$(cat ~/.passwd)" https://example.com puts the pw in the process list
  4. curl -u user https://example.com prompts for the pw
  5. curl --netrc-file ~/.netrc https://example.com requires a file

#4 is secure, but I might run this command hundreds of times a day, so it's tedious. #5 is close to secure, but that file could be read by somebody with root access.

The cURL man page says (note the bold text):

-u/--user <user:password>

Specify the user name and password to use for server authentication. Overrides -n/--netrc and --netrc-optional.

If you just give the user name (without entering a colon) curl will prompt for a password.

If you use an SSPI-enabled curl binary and do NTLM authentication, you can force curl to pick up the user name and password from your environment by simply specifying a single colon with this option: -u :.

I've tried setting $USER and $PASSWORD (and $CURLOPT_PASSWORD and others) in the environment, but cURL doesn't pick up either of them when invoked as curl -u : https://example.com (nor does it work without the -u :).

I'm not doing NTLM, so this doesn't work. Unless I'm missing something.

 

Is there a way to pass credentials to curl solely through the environment?

 

(Workaround moved to an answer)

Adam Katz
  • 14,455
  • 5
  • 68
  • 83
  • 1
    You don't need perl for that you could just a shell snippet there just as well but note that the process environment is available to `root` just as much as a file is (though for a shorter timeframe potentially). – Etan Reisner Nov 19 '15 at 03:47
  • SSPI and NTLM are both Windows technologies. Presumably the `curl` man page is not talking about retrieving the password from a POSIX-style environment. – chepner Nov 19 '15 at 14:51
  • @EtanReisner - I don't know of a way to do this in Bash or POSIX shell that doesn't place the value of the environment variable in the process list at least for a fraction of a second; e.g. `export FOO=bar; sh -xc 'echo $FOO'` will show that it runs `echo bar`. The difficulty of root obtaining that variable from the process environment is acceptably difficult enough to satisfy me unless a better option is presented. – Adam Katz Nov 19 '15 at 17:21
  • 1
    I think `declare -p USER | sed 's/^[^=]*="//;s/"[^"]*$//;s/\\"/"/'` satisfies but certainly isn't better than the perl, etc. solution. Do heredocs/herestrings show up in the command? (I don't recall offhand.) If not those would work too. – Etan Reisner Nov 19 '15 at 18:23
  • 3
    It took me 29 seconds to replace `curl` with a wrapper script that did a strace dump on the original binary, circumventing every single of these techniques including #4. By design, you can not hide from local root. – that other guy Nov 19 '15 at 19:19
  • Good thinking, @EtanReisner. I've updated the workaround code to use the `set` builtin (which alleviates the need to broadly expose the variable via `export`) and `awk` (faster and more available than `perl`). I'm not sure about here{doc,string}s, those might also work. A note on that front: `curl --netrc-file /dev/stdin` doesn't work :-( – Adam Katz Nov 19 '15 at 19:20
  • @thatotherguy: yeah, you can only be so thorough. I'm not looking for "perfect" secrecy, just "good enough." – Adam Katz Nov 19 '15 at 19:21
  • @EtanReisner: Herestring doesn't work: `secret=99123; sleep <<< $secret` gives an error. Heredoc is not hidden: `secret=99123; sleep $(cat < – Adam Katz Nov 20 '15 at 05:02
  • @AdamKatz Those aren't the right tests. The herestring version failed because you didn't give `sleep` an argument. `secret=99123; sleep 60 <<<$secret` should run (and I don't see anything in the process listing). Similarly your heredoc version is ignoring the heredoc and just shoving it all into a command substitution which obviously ends up on the command line (that's your #3). – Etan Reisner Nov 20 '15 at 05:18
  • @EtanReisner: I do not see any other way to use herestring/heredoc since `curl --netrc-file /dev/stdin` does not work. Feel free to propose something. – Adam Katz Nov 20 '15 at 05:56
  • 1
    Something like `--netrc-file <(cat <<<"machine $SRV login $USER password $PASSWORD")` possibly? But once you've hit a sub-shell/etc. avoiding `sed` isn't really worth much. – Etan Reisner Nov 20 '15 at 13:18
  • That's the ticket. I cleaned that up and turned it into an answer. I also did more security tests. Looks like the FD is a one-time use and the herestring is invisible to the process list. Thanks! – Adam Katz Nov 21 '15 at 11:15

6 Answers6

40

This bash solution appears to best fit my needs. It's decently secure, portable, and fast.

#!/bin/bash
SRV="example.com"
URL="https://$SRV/path"
curl --netrc-file <(cat <<<"machine $SRV login $USER password $PASSWORD") "$URL"

This uses process substitution (<( command ) runs command in a sub-shell to populate a file descriptor to be handed as a "file" to the parent command, which in this case is curl). The process substitution contains a here-string (cat <<< text, a variant of echo text that won't put anything into your process list), creating a file descriptor for the netrc file in order to pass credentials to the remote web server.

The security afforded by process substitution is actually pretty sound: its file descriptor is not a temporary file and is unavailable from even other calls in the same shell instance, so this appears secure in this context; an adversary would have to dig through memory or launch a complicated attack to find its contents. Since the $PASSWORD environment variable is also in memory, this should not increase the attack surface.

As long as you haven't used export PASSWORD, a trick like ps ewwp $$ shouldn't reveal the password (as noted in this comment). It'd also be wise to use some less obvious variable name.

Here is a simplified insecure version of the above code that may help explain how it works:

#!/bin/sh
# INSECURE VERSION, DO NOT USE
SRV=example.com
URL="https://$SRV/path"
TMP=$(mktemp)
printf "machine %s login %s password %s\n" "$SRV" "$USER" "$PASSWORD" > "$TMP"
curl --netrc-file "$TMP" "$URL"
rm -f "$TMP"

This insecure version has lots of flaws, all of which are solved in the previous version:

  • It stores the password in a file (though that file is only readable to you)
  • It very briefly has the password in a command line
  • The temporary file remains until after curl exits
  • Ctrl+c will quit without removing the temporary file

Some of that could be solved by:

#!/bin/sh
SRV=example.com
URL="https://$SRV/path"
TMP=$(mktemp /dev/shm/.XXXXX)  # assumes /dev/shm is a ramdisk
trap "rm -f $TMP" 0 18
cat << EOF > "$TMP"
machine $SRV login $USER password $PASSWORD
EOF
(sleep 0.1; rm -f "$TMP") &  # queue removing temp file in 0.1 seconds
curl --netrc-file "$TMP" "$URL"

I consider this version to be messy, suboptimal, and possibly less secure (though it is more portable). It also requires a version of sleep that understands decimals (and 0.1 seconds may be too fast if the system is heavily loaded).

 


I had originally posted a workaround that included a perl one-liner in my question, then (with help from Etan Reisner) I worked through a few better methods before settling on this here-string method, which is both lighter-weight (faster) and more portable.

At this point, it's elegant enough that I'd consider it the "answer" rather than an "ugly workaround," so I've migrated it to be this official answer. I've given @ghoti a +1 for his answer, which correctly states that cURL's command line program is incapable of doing what I want on its own, but I'm not "accepting" that answer because it doesn't help solve the issue.

Adam Katz
  • 14,455
  • 5
  • 68
  • 83
  • 2
    `<(cat <<<"$PASSWORD")` can be safely replaced by `<(echo "$PASSWORD")` if your shell's `echo` command is a builtin. The $PASSWORD will not appear in the process table. In bash, whether echo is a builtin can be tested using `type -t echo`. – Robin A. Meade May 26 '21 at 19:59
  • Unfortunately this does not seem work if you use `-L/--location`- curl seems to try reading the file again after redirect, but the process already exited... (curl 7.81.0). – ciis0 Jun 10 '22 at 12:08
  • 1
    If you use the same approach with `-K/--config` it does work! ([cf.](https://stackoverflow.com/a/53242234)) `curl --config <(echo user=username:password) --location https://example.com/redirect?id=42` – ciis0 Jun 10 '22 at 12:13
  • @RobinA.Meade I would recommend using the `printf` builtin instead of `echo`, since the former is more portable (i.e. `printf '%s' "$PASSWORD"`) and won't behave oddly if the password is something like `-e` or `-n`. – Pedro A Dec 21 '22 at 15:18
  • @RobinA.Meade – `<(echo "$PASSWORD")` reveals the password given `set -x` while my version does not. @​PedroA – Same issue as Robin's tweak, plus this is bash, not POSIX sh; I'm pretty sure bash has had `echo` as a builtin from its inception. – Adam Katz Dec 28 '22 at 18:42
9

Is there a way to pass credentials to curl solely through the environment?

No, I don't think there is.

The CURLOPT_USERPWD documentation I think describes what you need, but this is an option that would be available using the curl library in some other language. PHP, Perl, C, etc.

The curl binary you run from your shell is just another front end on that library, but the way things like CURLOPT_USERPWD get passed to the library through the curl binary is by use of command line options on the binary.

You could theoretically write your own binary as a front end to the curl library, and write in support for environment variables.

You could alternately hack environment support as you're hoping to see it into the existing curl binary, and compile your own with local functions.

Beware, though, that even environment variables may be leaked by your shell into the process table. (What do you see when you run ps ewwp $$?)

Perhaps a .netrc file with restricted permissions will be the safest way to go. Perhaps you will need to generate a temporary .netrc file to be used by the --netrc-file curl option.

I think you either have to pick the least risky solution for your environment, or write something in a real language that does security properly.

ghoti
  • 45,319
  • 8
  • 65
  • 104
  • 1
    `foo=bar; export foo; ps ewwp $$ |grep -i foo` finds the variable in `bash` but interestingly finds nothing in `zsh` (my shell of choice). (Without `export`, `ps ewwp $$` doesn't find the variable in `bash` or `zsh`, but `perl` doesn't pick it up either, making it useless.) The bash/zsh process substitution (`<(…)`) in my workaround appears to do a good job of restricting access to that very temporary "file" (though I'm sure it still lives in memory). – Adam Katz Nov 19 '15 at 18:53
  • I've moved my workaround to [an answer](https://stackoverflow.com/questions/33794842/forcing-curl-to-get-a-password-from-the-environment#33818945) and alleviated the need for `export` which makes the variable invisible to `ps ewwp` – Adam Katz Nov 20 '15 at 04:29
9

User "Tom, Bom" provides a decent solution for this here: https://coderwall.com/p/dsfmwa/securely-use-basic-auth-with-curl

curl --config - https://example.com <<< 'user = "username:password"'

This prevents passwords from showing up in the process list, though this does not specifically address the OP's original question:

Is there a way to pass credentials to curl solely through the environment?

I still give points to @ghoti for giving a more comprehensive and informative answer.

sfgeorge
  • 356
  • 3
  • 6
5

Previous answers are correct, the best option is using -n for curl(assuming you on linux):

  1. create a file (use your own favorite editor)

vi ~YOUR_USER_NAME/.netrc

  1. add the followings

machine example.com login YOUR_USER_NAME password THE_ACTUAL_PASSWORD

  1. run

curl -n https://example.com/some_end_point

grepit
  • 21,260
  • 6
  • 105
  • 81
  • 1
    This uses the filesystem rather than the environment, which is not what this question asks. – Adam Katz Dec 14 '18 at 17:15
  • @AdamKatz so your opinion is all the previous answers are incorrect...I provided detailed step by step what some of the previous answers also recommended !!! – grepit Dec 14 '18 at 19:13
  • 2
    This is the only answer that suggests storing the password _in a file_. My question was about how to use a password _in the environment_, **specifically so that it would never hit the filesystem**. None of the other answers here involve an actual file; they merely use a shell construct that is treated _like_ a file. – Adam Katz Dec 14 '18 at 21:42
  • “netrc-file” is actually stores credentials in a file as you .. yourself answered your own question and have 13 up vote !! – grepit Dec 15 '18 at 02:47
  • 1
    `--netrc-file` refers to a "file" to read in place of `~/.netrc`. However, my answer provides a [process substitution](https://en.wikipedia.org/wiki/Process_substitution) to populate a [file descriptor](https://en.wikipedia.org/wiki/File_descriptor), **which is not a file**, never exists on any filesystem, and is not available to any other user (including root) or even to the invoking user from another shell process. – Adam Katz Dec 16 '18 at 20:10
  • 1
    the solution you recommending is actually far worse than storing in a file because anyone that does ps -ef|grep curl will see the password assuming that curl command is executed during that time...it's something that you almost should never do in linux – grepit Dec 16 '18 at 21:54
  • 3
    I tested that (though feel free to actually test for yourself and share any contradictory findings). As explicitly stated in my answer, **here-string contents do not show up in process lists** (`ps -ef|grep curl` will show you something like `curl --netrc-file /proc/self/fd/18 http://…`). That's the whole reason this works. Please read my answer in full, as all of this is discussed there in great detail. There's a little more if you read the comments in my question, my answer, and ghoti's answer. – Adam Katz Dec 16 '18 at 23:03
3

Curl uses SPACE and TAB as delimiters when it parses the tokens in the netrc file:

https://github.com/curl/curl/blob/bc5a0b3e9f16a431523ae54822adc38c3a396a26/lib/netrc.c#L122

The --netrc-file approach therefore can't handle a SPACE or TAB in the password.

Test of SPACE in password

SRV="httpbin.org"
URL="https://$SRV/basic-auth/username/pass%20word"
USERNAME=username
PASSWORD='pass word'
curl -v --netrc-file <(echo "machine $SRV login $USERNAME password $PASSWORD") "$URL"

Result: ❌ FAIL

WARNING: If your shell's echo command is not a builtin, the above curl invocation will leak $PASSWORD into the process table momentarily. In bash, whether echo is a builtin or not can be tested with type -t echo. WORKAROUND: Use cat and a here-string: Replace <(echo "string") with <(cat <<< "string"). This warning applies to all the examples in this answer.

Test of TAB in password

SRV="httpbin.org"
URL="https://$SRV/basic-auth/username/pass%09word"
USERNAME=username
PASSWORD=$'pass\tword'
curl -v --netrc-file <(echo "machine $SRV login $USERNAME password $PASSWORD") "$URL"

Result: ❌ FAIL

Test of double quotation mark " in password

SRV="httpbin.org"
URL="https://$SRV/basic-auth/username/pass%22word"
USERNAME=username
PASSWORD='pass\"word'
curl -v --netrc-file <(echo "machine $SRV login $USERNAME password $PASSWORD") "$URL"

Result: ❌ FAIL

A more robust way

@sfgeorge correctly points out that the -K,  --config <file> option, with <file> set to -, could be used to supply the password on STDIN. Using STDIN for this purpose, however, would preclude using STDIN for other purposes, like to POST data using --data @-.

Fortunately, we can use process substitution instead of STDIN. A process substitution expands to a filename and can be used where a filename is expected.

Test of SPACE in password

USERNAME=username
PASSWORD='pass word'
curl -v \
  -K <(echo "user: \"$USERNAME:$PASSWORD\"") \
  "https://httpbin.org/basic-auth/username/pass%20word"

Result: ✅ SUCCESS

Test of TAB in password

USERNAME=username
PASSWORD=$'pass\tword'
curl -v \
  -K <(echo "user: \"$USERNAME:$PASSWORD\"") \
  "https://httpbin.org/basic-auth/username/pass%09word"

Result: ✅ SUCCESS

And, for extra robustness, let's make it handle double quotation marks in the password as well.

Test of double quotation mark " in password

USERNAME=username
PASSWORD=$'pa s\ts"wo$rd'

# Build 'user' option
USER_OPT="$USERNAME:$PASSWORD"
USER_OPT=${USER_OPT//\\/\\\\} # Escape `\`
USER_OPT=${USER_OPT//\"/\\\"} # Escape `"`
USER_OPT="user: \"${USER_OPT}\""

curl -v \
  -K <(echo "$USER_OPT") \
  "https://httpbin.org/basic-auth/username/pa%20s%09s%22wo%24rd"

Result: ✅ SUCCESS

I threw in an emoji for good measure.

Robin A. Meade
  • 1,946
  • 18
  • 17
  • 1
    I guess this isn't the top/accepted answer since it's not technically an ENV, but it's a great and thorough answer. I needed to handle passwords with `$` in them and this is great. I can just save a `creds.curl` file and run `curl -K creds.url (whatever)`. – Mat Schaffer May 26 '21 at 04:29
0

I'd like to point out that even bash here-string syntax (<<<hello) creates a file in /tmp that, while it's only there for an instant, is still susceptible to timing attacks. Running strace bash -c '/bin/cat <<<hello' reveals how it works:

19376 open("/tmp/sh-thd-1651575757", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, 0600) = 3
19376 write(3, "hello", 5)              = 5
19376 write(3, "\n", 1)                 = 1
19376 open("/tmp/sh-thd-1651575757", O_RDONLY) = 4
19376 close(3)                          = 0
19376 unlink("/tmp/sh-thd-1651575757")  = 0
19376 dup2(4, 0)                        = 0
19376 close(4)                          = 0
samwyse
  • 2,760
  • 1
  • 27
  • 38