PuTTY is one of the oldest and most popular SSH clients, originally for Windows, but now available on several platforms. It has won corporate support and endorsement, and is prepared and bundled within several third-party repositories.

Unfortunately, the 0.74 stable PuTTY release does not safely guard plain-text passwords provided to it via the -pw command line option for the psftp, pscp, and plink utilities as the documentation clearly warns. There is evidence within the source code that the authors are aware of the problem, but the exposure is confirmed on Microsoft Windows, Oracle Linux, and the package prepared by the OpenBSD project.

After discussions with the original author of PuTTY, Simon Tatham developed a new -pwfile option, which will read an SSH password from a file, removing it from the command line. This feature can be backported into the current 0.76 stable release. Full instructions for applying the backport and a .netrc wrapper for psftp are presented, also implemented in Windows under Busybox.

While the -pw option is attractive for SSH users who are required to use passwords (and forbidden from using keys) for scripting activities, the exposure risk should be understood for any use of the feature. Users with security concerns should obtain the -pwfile functionality, either by applying a patch to the 0.76 stable release, or using a snapshot release found on the PuTTY website.


The psftp, pscp, and plink utilities are able to accept a password on the command line, as their usage output describes:

$ psftp -h
PuTTY Secure File Transfer (SFTP) client
Release 0.76
Usage: psftp [options] [user@]host
Options: -V print version information and exit -pgpfp print PGP key fingerprints and exit -b file use specified batchfile -bc output batchfile commands -be don't stop batchfile processing if errors -v show verbose messages -load sessname Load settings from saved session -l user connect with specified username -P port connect to specified port -pw passw login with specified password -1 -2 force use of particular SSH protocol version -ssh -ssh-connection force use of particular SSH protocol variant -4 -6 force use of IPv4 or IPv6 -C enable compression -i key private key file for user authentication -noagent disable use of Pageant -agent enable use of Pageant -no-trivial-auth disconnect if SSH authentication succeeds trivially -hostkey keyid manually specify a host key (may be repeated) -batch disable all interactive prompts -no-sanitise-stderr don't strip control chars from standard error -proxycmd command use 'command' as local proxy -sshlog file -sshrawlog file log protocol details to a file -logoverwrite -logappend control what happens when a log file already exists

The manual pages for the psftp, pscp, and plink clients bundled within the EPEL package clearly document the risk of exposure with this option:

$ man psftp | sed -n '/pw password/,/commands/p' -pw password Set remote password to password. CAUTION: this will likely make the password visible to other users of the local machine (via commands such as `w').

Note that this documentation from the UNIX manual page above is not included in the Windows MSI installer. While this warning is found in the general documentation, the risk to Windows users is not prominent: -pw: specify a password

A simple way to automate a remote login is to supply your password on the command line. This is not recommended for reasons of security. If you possibly can, we recommend you set up public-key authentication instead.

This warning is genuine, as is easily demonstrated on Linux:

$ psftp -pw foobar4.foobar cfisher@localhost
Using username "cfisher".
Remote working directory is /home/cfisher
psftp> !sh $ ps ax | grep psftp 7490 pts/1 S 0:00 psftp -pw foobar4.foobar cfisher@localhost sh-4.2$ cat /proc/7490/cmdline; echo

Shell scripts relying upon the -pw argument to automate psftp, pscp, or plink do so at the cost of credential exposure, as any shell account can see the process list, on Linux via /proc/*/cmdline and on OpenBSD by other mechanisms.

Windows users will likely contend that this issue does not impact their platform; they are mistaken, as a few clicks in task manager will show:

PuTTY Task Manager

Passwords used with the -pw option should be considered exposed and changed if any unprivileged users have had access to the process list.

Incomplete Remediation

While a common but imperfect solution, it appears that modern programs wipe passwords from their command lines by writing into the “argument vector” as defined in the C programming language. Below is an example:

$ cat clipurge.c
#include <stdio.h>
#include <string.h>
#define BUF 1024 int main(int argc, char **argv)
{ int c; char junk[BUF]; for(c = 1; c < argc; c++) if(!strcmp("-pw", argv[c])) { int d = 0; while(*(d + argv[c + 1])) *(d++ + argv[c + 1]) = '*'; } fgets(junk, BUF, stdin);

We can compile and test this code to see the removal:

$ cc -o clipurge clipurge.c $ ./clipurge foo bar -pw baz bada bing

From another shell, check the process list:

$ ps ax | grep clipurge
13500 pts/1 S+ 0:00 ./clipurge foo bar -pw *** bada bing

The technique has been tested to work on Oracle Linux and OpenBSD (notably, it does not work on legacy HP-UX, where the old hide.c can be used instead). Windows, unfortunately, was not remediated in testing with the Cygwin GCC compiler.

Since all of the PuTTY utilities are written in C, we can add this code to their source and prepare new binaries if a capable C compiler is available.

To create a patched version of the psftp, pscp, and plink utilities on applicable platforms, place the following files in your home directory:

$ cat ~/cmdline.patch
--- cmdline.c.orig	2021-09-26 11:15:52.386305592 -0500
+++ cmdline.c	2021-09-26 11:16:08.359152634 -0500
@@ -163,7 +163,7 @@ settings_set_default_port(port); conf_set_int(conf, CONF_port, port); }
+extern char **globargv; extern int globargc; int cmdline_process_param(const char *p, char *value, int need_save, Conf *conf) {
@@ -575,12 +575,12 @@ if (conf_get_int(conf, CONF_protocol) != PROT_SSH) cmdline_error("the -pw option can only be used with the " "SSH protocol");
- else {
+ else { int c; cmdline_password = dupstr(value); /* Assuming that `value' is directly from argv, make a good faith * attempt to trample it, to stop it showing up in `ps' output * on Unix-like systems. Not guaranteed, of course. */
- smemclr(value, strlen(value));
+ smemclr(value, strlen(value)); for(c=1;c<globargc;c++) if(!strcmp("-pw", globargv[c])) {int d=0; char *a=globargv[c+1]; while(*(d+a)) {*(d+a) = (d==0)?'X':0; d++;} } } } 

$ cat ~/uxsftp.patch
--- unix/uxsftp.c.orig	2021-09-26 11:07:24.900243423 -0500
+++ unix/uxsftp.c	2021-09-26 11:08:13.039656553 -0500
@@ -566,13 +566,13 @@ void platform_psftp_pre_conn_setup(LogPolicy *lp) {} const bool buildinfo_gtk_relevant = false;
+int globargc; char **globargv; /* * Main program: do platform-specific initialisation and then call * psftp_main(). */ int main(int argc, char *argv[])
+{ globargc=argc; globargv=argv; uxsel_init(); return psftp_main(argc, argv); }

$ cat ~/uxplink.patch
--- unix/uxplink.c.orig	2021-09-26 11:07:34.101329314 -0500
+++ unix/uxplink.c	2021-09-26 11:08:44.210306055 -0500
@@ -654,7 +654,7 @@ return false; /* terminate main loop */ return true; }
+int globargc; char **globargv; int main(int argc, char **argv) { int exitcode;
@@ -664,7 +664,7 @@ bool just_test_share_exists = false; struct winsize size; const struct BackendVtable *backvt;
+globargc=argc; globargv=argv; /* * Initialise port and protocol to sensible defaults. (These * will be overridden by more or less anything.)

Notice above the text, “Assuming that ‘value’ is directly from argv, make a good faith attempt to trample it, to stop it showing up in ‘ps’ output on Unix-like systems. Not guaranteed, of course.” Unfortunately, this good faith attempt appears insufficient.

The patch presented here simply makes the original argv available in the context where the wipe is designed to occur, then wipes all the -pw parameters found. It might be preferable to determine exactly why the intended wipe of the incoming value does not perform as expected.

To apply these source code patches, download the 0.76 release of PuTTY, unpack it, change directory to its top level, and run the following commands:

$ patch -p0 < ~/cmdline.patch
patching file cmdline.c $ patch -p0 < ~/uxsftp.patch
patching file unix/uxsftp.c $ patch -p0 < ~/uxplink.patch
patching file unix/uxplink.c

On systems with modern GCC or equivalent options, run this custom configuration command which enables all of the compiler safety controls (syntax confirmed on OpenBSD clang; note that -O3 has caused crashes):

CFLAGS='-O2 -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fpic -pie' LDFLAGS='-Wl,-z,relro,-z,now -Wl,-z,now' ./configure

Then run make to trigger the build:

$ make

When the compiler finishes, the following programs should be visible:

$ ls -l plink pscp psftp
-rwxr-xr-x. 1 fishecj itg 837424 Sep 24 12:17 plink
-rwxr-xr-x. 1 fishecj itg 825336 Sep 24 12:17 pscp
-rwxr-xr-x. 1 fishecj itg 838400 Sep 24 12:17 psftp

If you have the hardening-check utility, you can confirm that the programs were compiled with safety controls:

$ hardening-check plink pscp psftp
Position Independent Executable: yes
Stack protected: yes
Fortify Source functions: yes (some protected functions found)
Read-only relocations: yes
Immediate binding: yes
Position Independent Executable: yes
Stack protected: yes
Fortify Source functions: yes (some protected functions found)
Read-only relocations: yes
Immediate binding: yes
Position Independent Executable: yes
Stack protected: yes
Fortify Source functions: yes (some protected functions found)
Read-only relocations: yes
Immediate binding: yes

Test the patched psftp, pscp, and plink:

$ printf 'Password: '; stty -echo; read Pass; stty echo; echo
Password: $ ./psftp -pw "$Pass" $USER@localhost
Using username "fishecj".
Remote working directory is /home/cfisher
psftp> !sh $ ps ax | grep psftp
24095 pts/0 S 0:00 ./psftp -pw X cfisher@localhost $ cat /proc/24095/cmdline; echo

This is not a completely effective patch; it is still able to leak a password. To demonstrate how this can take place, run the following shell script fragment:

$ while true; do ps ax | grep psftp | grep -v grep >> log; done

In another shell, run this fragment:

$ while true; do echo quit | ./psftp -pw "$Pass" $USER@localhost; done

Eventually, the password will appear in the log:

19681 pts/0 R+ 0:00 ./psftp -pw foobar4.foobar cfisher@localhost
19681 pts/0 R+ 0:00 ./psftp -pw X cfisher@localhost

For a brief time at program startup, the password is present on the command line before it is overwritten, and can be captured. A simple shell script fragment was able to record it with enough attempts, engaging in a race condition.

A carefully-coded C application that leveraged inotify might be able to dramatically increase the success rate. Applications that require the use of command line passwords cannot completely escape vulnerability by manipulating argv. Still, this patch provides the imperfect obfuscation that the PuTTY authors intended.

Secure Backport with .netrc

Since manipulation of argv cannot ensure complete security for sensitive credentials, Simon Tathum has added a new feature, -pwfile, which will remove passwords from the command line by reading them from a named file.

This feature is currently available in the snapshot release on the PuTTY website, and the Windows psftp binary has been successfully tested with this new functionality.

For UNIX users, the new feature can be applied as a patch to the stable 0.76 release. Place Simon’s code in the following file in your home directory:

$ cat ~/simon.patch
--- cmdline.c.orig	2021-10-01 09:33:17.000000000 -0500
+++ cmdline.c	2021-10-01 09:33:38.000000000 -0500
@@ -584,6 +584,32 @@ } } + if (!strcmp(p, "-pwfile")) {
+ RETURN(2);
+ /* We delay evaluating this until after the protocol is decided,
+ * so that we can warn if it's of no use with the selected protocol */
+ if (conf_get_int(conf, CONF_protocol) != PROT_SSH)
+ cmdline_error("the -pwfile option can only be used with the "
+ "SSH protocol");
+ else {
+ Filename *fn = filename_from_str(value);
+ FILE *fp = f_open(fn, "r", false);
+ if (!fp) {
+ cmdline_error("unable to open password file '%s'", value);
+ } else {
+ cmdline_password = chomp(fgetline(fp));
+ if (!cmdline_password) {
+ cmdline_error("unable to read a password from file '%s'",
+ value);
+ }
+ fclose(fp);
+ }
+ filename_free(fn);
+ }
+ }
+ if (!strcmp(p, "-agent") || !strcmp(p, "-pagent") || !strcmp(p, "-pageant")) { RETURN(1);

To apply this patch, unpack a clean copy of the 0.76 PuTTY source code, without any of the patches in the previous section (the previous patches can be applied with Simon’s patch, but are not completely effective due to the demonstrated race condition; for this reason, users requiring assured security should use only Simon’s patch, as a wipe of argv cannot be completely effective, and is not helpful on Windows). With pristine source and the patch in place, configure and execute your build:

$ cd putty-0.76/ $ patch -p0 < ~/simon.patch
patching file cmdline.c $ CFLAGS='-O2 -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fpic -pie' LDFLAGS='-Wl,-z,relro,-z,now -Wl,-z,now' ./configure $ make

When the configuration and build have completed, the psftp, pscp, and plink executables now implement -pwfile. Rename the new SFTP client, to distinguish it from any version installed in an OS package:

$ mv psftp pwpsftp

In testing this functionality, I will use a POSIX shell script as a wrapper to implement .netrc capability for PuTTY psftp.

The .netrc format has long been used by classic FTP clients, and allows credential storage for many accounts in a secured text file, usually one’s home directory. In this example, I will place the following example for testing:

$ echo 'machine login fishecj password foobar4.foobar
default login foo password bar' > ~/.netrc $ chmod 600 ~/.netrc

A competitor to psftp, known as Curl or cURL, has implemented a --netrc option for many years which can be used with SFTP transfers. PuTTY is a superior product to curl for many uses, as it implements a command interface that is similar to the original FTP which eases retooling of scripts, and PuTTY’s cryptography is currently superior in all respects.

PuTTY’s cryptography is specifically better than the version 7.79.1 curl-amd64 binary found on the download site. The curl version tested does not implement any AEAD ciphers (as are required for TLS 1.3 and are best practices for SSH). Also, of the supported MACs, none are of the “Encrypt-then-MAC” (ETM) variety. In detail, the version 7.79.1 curl-amd64 implements the following ciphers: aes256-ctr, aes192-ctr, aes128-ctr, aes256-cbc, aes192-cbc, aes128-cbc, rijndael-cbc@lysator.liu.se, arcfour, cast128-cbc, 3des-cbc, blowfish-cbc, and arcfour128. In addition, the referenced curl version implements the following MACs: hmac-sha2-512, hmac-sha2-256, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1, hmac-sha1-96, hmac-md5, and hmac-md5-96.

Place the following script, and the previous pwpsftp executable, in your PATH for testing, and mark the script with 755 permissions on UNIX:

$ cat pwpsftp
#!/bin/sh # .netrc wrapper for PuTTY psftp -pwfile H= L= P= D= DL= DP= t="$1" unset IFS # Host Login Password Default/L/P target
set -eu; shift # http://redsymbol.net/articles/unofficial-bash-strict-mode/ sftp () { PWF="$(mktemp)"; trap "rm -fv "$PWF"" EXIT; printf %s "$P" > "$PWF" [ -t 9 ] && exec 9<&-; pwpsftp -pwfile "$PWF" "${L}@${t}" "$@"; exit $?; } newL () { [ "$t" = "$H" -a "$L" -a "$P" ] && sftp "$@" [ "$D" ] && DL="$L" DP="$P"; true; } while read line <&9 # This feeds into psftp's stdin without alternate #< fd
do for thisword in $line do case "$thisword" in machine) newL "$@"; D= L= P= H=_ continue ;; default) newL "$@"; H= L= P= D=_ continue ;; login) L=_ continue ;; password) P=_ continue ;; esac [ "X$H" = X_ ] && H="$thisword" [ "X$L" = X_ ] && L="$thisword" [ "X$P" = X_ ] && P="$thisword" done
done 9< ~/.netrc # This feeds into psftp's stdin without alternate #< fd newL "$@"; [ "$DL" -a "$DP" ] && { L="$DL" P="$DP"; sftp "$@"; } # Check default

The script will extract a specific password from the .netrc and store it in a file created by the mktemp utility, which attempts to ensure that the temporary file “is only readable and writable by its owner.” The temporary file will be in place for the duration of the SFTP session, and it’s deletion will be reported at the close. A test use is below:

$ psftprc
Using username "fishecj".
Remote working directory is /home/fishecj
psftp> !sh sh-4.2$ ps ax | grep psftp 9765 pts/1 S 0:00 /bin/dash /home/fishecj/putty-0.76/psftprc 9767 pts/1 S 0:00 pwpsftp -pwfile /tmp/tmp.VqMIsHfkCQ fishecj@ sh-4.2$ ls -l /tmp/tmp.VqMIsHfkCQ
-rw-------. 1 fishecj itg 14 Oct 1 13:13 /tmp/tmp.VqMIsHfkCQ sh-4.2$ cat /tmp/tmp.VqMIsHfkCQ; echo
foobar4.foobar $ cat /proc/9767/cmdline; echo
pwpsftp-pwfile/tmp/tmp.VqMIsHfkCQfishecj@ sh-4.2$ exit psftp> exit
removed ‘/tmp/tmp.VqMIsHfkCQ’

Notice above the use of dash, which closely adheres to POSIX shell syntax and tolerates (very nearly) no BASHisms, indicating that this script should run in most UNIX shells. Also observe the verbose removal of the temporary file at the end of the SFTP session, triggered by the EXIT trap. A subshell launched as a background process that sleeps for a few seconds, then deletes the temporary file would increase security, especially for SFTP sessions of long duration.

The .netrc format may also specify a default login, which will be used on all connections that are not otherwise explicitly defined in the file:

$ psftprc
Using username "foo".
Remote working directory is /tmp
psftp> quit
removed ‘/tmp/tmp.0jQCj1xjMr’

The psftprc script also returns the exit code of the psftp binary, allowing session failures to be detected and retried. A simple test is to shut down the master sshd on the target, then configure and launch a batch transfer:

$ echo 'cd tinyssh
dir' > stuff $ until psftprc -b stuff; do sleep 5; done
FATAL ERROR: Connection refused
removed ‘/tmp/tmp.8eQUXu3ri1’
FATAL ERROR: Connection refused
removed ‘/tmp/tmp.7lzVesQsDk’

When the remote sshd is restarted, the loop terminates. This technique is extremely helpful with unreliable connections, or transfers that must be retried on any and all failures.

FATAL ERROR: Connection refused
removed ‘/tmp/tmp.gkcHN5O0yC’
Using username "foo".
Remote working directory is /tmp
Remote directory is now /tmp/tinyssh
Listing directory /tmp/tinyssh
drwxr-xr-x 4 fishecj itg 102 Jun 25 2020 .
drwx------ 27 fishecj itg 4096 Oct 1 14:34 ..
-rw-r--r-- 1 fishecj itg 233155 Jun 24 2020 20190101.tar.gz
drwxr-xr-x 10 fishecj itg 4096 Jun 24 2020 tinyssh-20190101
drwxr-xr-x 2 fishecj itg 4096 Jun 25 2020 tinyssh-convert
-rwxr-xr-x 1 fishecj itg 1861 Jun 25 2020 tinyssh-keyconvert
removed ‘/tmp/tmp.ugODaSfWPm’

This script is also functional with the Windows Busybox port:

C:Usersfishecj>busybox64 sh psftprc
Using username "oracle".
Remote working directory is /home/oracle
psftp> pwd
Remote directory is /home/oracle
psftp> quit
removed 'C:/Users/FISH~1/AppData/Local/Temp/tmp.a18944' C:Usersfishecj>copy con stuff
cd Ora19/OPatch
^Z 1 file(s) copied. C:Usersfishecj>busybox64 sh psftprc -b stuff
Using username "oracle".
Remote working directory is /home/oracle
Remote directory is now /home/oracle/Ora19/OPatch
Listing directory /home/oracle/Ora19/OPatch
drwxr-x--- 14 oracle dba 4096 Apr 21 2020 .
drwxr-xr-x 71 oracle dba 4096 May 12 2020 ..
-rw-r----- 1 oracle dba 2980 Apr 12 2019 README.txt
drwxr-x--- 6 oracle dba 64 Apr 12 2019 auto
drwxr-x--- 2 oracle dba 30 Apr 12 2019 config
-rwxr-x--- 1 oracle dba 589 Apr 12 2019 datapatch
drwxr-x--- 2 oracle dba 86 Apr 12 2019 docs
-rwxr-x--- 1 oracle dba 23550 Apr 12 2019 emdpatch.pl
drwxr-x--- 2 oracle dba 4096 Apr 21 2020 jlib
drwxr-x--- 5 oracle dba 4096 Aug 16 2018 jre
drwxr-x--- 9 oracle dba 4096 Apr 12 2019 modules
drwxr-x--- 5 oracle dba 54 Apr 12 2019 ocm
-rwxr-x--- 1 oracle dba 48493 Apr 12 2019 opatch
-rwxr-x--- 1 oracle dba 2551 Apr 12 2019 opatch.pl
-rwxr-x--- 1 oracle dba 4290 Apr 12 2019 opatch_env.sh
-rwxr-x--- 1 oracle dba 1442 Apr 12 2019 opatchauto
-rwxr-x--- 1 oracle dba 393 Apr 12 2019 opatchauto.cmd
drwxr-x--- 4 oracle dba 59 Apr 12 2019 opatchprereqs
-rwxr-x--- 1 oracle dba 3159 Apr 12 2019 operr
-rw-r----- 1 oracle dba 3177 Apr 12 2019 operr_readme.txt
drwxr-x--- 2 oracle dba 18 Apr 12 2019 oplan
drwxr-x--- 3 oracle dba 20 Apr 12 2019 oracle_common
drwxr-x--- 3 oracle dba 23 Apr 12 2019 plugins
drwxr-x--- 2 oracle dba 4096 Apr 21 2020 scripts
-rw-r----- 1 oracle dba 27 Apr 12 2019 version.txt
removed 'C:/Users/FISH~1/AppData/Local/Temp/tmp.a25248'

On Windows, it might be helpful to carefully examine the permissions on the .netrc file, as ACL access is much more complex than simple POSIX filesystem permissions. The following ACL adjustment might be prudent.

C:Usersfishecj>cacls .netrc /e /r Administrators
processed file: C:Usersfishecj.netrc

Also, on Windows, Simon’s patch has proven effective on Cygwin:

$ uname
CYGWIN_NT-10.0 $ echo foobar4.foobar > secret $ ./psftp -pwfile secret cfisher@myhost
Using username "cfisher".
Remote working directory is /home/cfisher
psftp> quit

Closing all PuTTY psftp security concerns will require the application of the methods presented in this section, and I would like to thank Simon Tatham for his contribution in allowing it to be written.


The consistent advice from the OpenSSH documentation is the avoidance of passwords when they cannot be entered interactively:

$ man sftp | sed -n '/automated/,/keygen/p' The final usage format allows for automated sessions using the -b option. In such cases, it is necessary to configure non-interactive authentica‐ tion to obviate the need to enter a password at connection time (see sshd(8) and ssh-keygen(1) for details).

Many psftp users and administrators do not heed this advice, and disallow keys. This comes at a cost.

While PuTTY’s stable release may be particularly difficult in the question of secure, scripted password handling (as the developers have repeatedly warned, and without aforementioned patches), we see here that common command line defenses can be circumvented for a much larger set of programs expressing sensitive content. A partial solution is to record credentials in a file, but so many incompatible formats have been adopted for this that choosing is not trivial (FTP/curl .netrc, OpenSSL’s -passin formats, a smbclient credential file, the particularly problematic web.config, etc.). Perhaps a credential cache provided by the OS, designed for security and ease of use and allowing either a standardized file format or sqlite, should be considered for the replacement of these patchwork solutions that are prone to mismanagement and abuse. It would also be helpful if the kernel offered administrators a method to securely hide argv.

The whole question of the standardization on SFTP for file transfer ignores so many problems: multiple failed protocol revisions in the SSH 1.X series, noted performance problems with the SFTP protocol, now-deprecated SCP, rsync advocacy despite license incompatibility, and the redesigns on the way. SSH offers file transfer as a sideline, not as a core component. Due to this turmoil, the choice of SSH as a file transfer standard seems premature; perhaps the approach that Wireguard took to VPN would be useful in designing a superior file transfer protocol and tool.

In any case, we should not advertise our passwords. Avoid doing so with PuTTY.

Once again, Simon Tathum deserves our thanks for his concise solution to this question of password exposure.

Posted by Contributor