If you want to change the password for some user from a script or a program, use the chpasswd
utility (specifically, /usr/sbin/chpasswd
).
If we assume you have written a secure privileged utility or application that can update user passwords, then you could use something like
int change_password(const char *username, const char *password)
{
FILE *cmd;
int status;
if (!username || !*username)
return errno = EINVAL; /* NULL or empty username */
if (!password || !*password)
return errno = EINVAL; /* NULL or empty password */
if (strlen(username) != strcspn(username, "\t\n\r:"))
return errno = EINVAL; /* Username contains definitely invalid characters. */
if (strlen(password) != strcspn(password, "\t\n\r"))
return errno = EINVAL; /* Password contains definitely invalid characters. */
/* Ensure that whatever sh variant is used,
the path we supply will be used as-is. */
setenv("IFS", "", 1);
/* Use the default C locale, just in case. */
setenv("LANG", "C", 1);
setenv("LC_ALL", "C", 1);
errno = ENOMEM;
cmd = popen("/usr/sbin/chpasswd >/dev/null 2>/dev/null", "w");
if (!cmd)
return errno;
fprintf(cmd, "%s:%s\n", username, password);
if (fflush(cmd) || ferror(cmd)) {
const int saved_errno = errno;
pclose(cmd);
return errno;
}
status = pclose(cmd);
if (!WIFEXITED(status))
return errno = ECHILD; /* chpasswd died unexpectedly. */
if (WEXITSTATUS(status))
return errno = EACCES; /* chpasswd failed to change the password. */
/* Success. */
return 0;
}
(but this is untested, because I personally would use the underlying POSIX <unistd.h>
I/O (fork()
, exec*()
, etc.) instead, for maximum control. See e.g. this example of how I handle a non-privileged operation, opening a file or URL in the user's preferred application. With privileged data, I'm much more paranoid. In particular, I'd check the ownership and mode of /
, /usr/
, /usr/sbin/
, and /usr/sbin/chpasswd
first, in that order, using lstat()
and stat()
, to ensure my application/utility is not being hoodwinked into executing a fake chpasswd
.)
The password is supplied to the function in clear text. PAM will handle encrypting it (per /etc/pam.d/chpasswd
), and the relevant system configuration used (per /etc/login.defs
).
All the standard security warnings about privileged operations apply. You do not want to pass usernames and passwords as command-line parameters, because they are visible in the process list (see ps axfu
output, for example). Environment variables are similarly accessible, and passed on to all child processes by default, so they're out as well. Descriptors obtained via pipe()
or socketpair()
are safe, unless you leak the descriptors to child processes. Having open FILE
streams to child processes when starting another, often leaks the descriptor between the parent and the first child to the latter child.
You must take all possible precautions to stop your application/utility being used to subvert user accounts. You either learn to do it when you first start thinking about writing code or scripts to manage user information, or you add to the horrible mound of insecure code exploited by nefarious actors trying to exploit innocent users, and will never learn to do it properly. (No, you will not learn it later. Nobody who says they'll add checks and security later on, actually does so. We humans just don't work that way. Security and robustness is either baked in from the get go, or slapped haphazardly later on like frosting: it changes nothing, even if it looks good.)
The setenv()
commands ensure chpasswd
is run in the default C locale. Andrew Henle pointed out that some sh
implementations could split the command path if IFS
environment variable was set to a suitable value, so we clear it to empty, just in case. The trailing >/dev/null 2>/dev/null
redirects its standard output and standard error to /dev/null
(nowhere), in case it might print an error message with sensitive information in it. (It will reliably exit with a nonzero exit status, if any errors do occur; and that's what we rely on, above.)