CVE-2017-15112: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: keycloak-httpd-client-install versions before 0.8 allow users to insecurely pass password through command line, leaking it via command history and process info to other local users.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 7.8
Published: 2018-01-20T00:29Z
Repository: https://github.com/jdennis/keycloak-httpd-client-install
Language: Other
Files changed: bin/keycloak-httpd-client-install, doc/keycloak-httpd-client-install.8, keycloak_httpd_client/keycloak_cli.py
Commit date: 2018-01-09 16:42:06 -0500
Commit message: CVE-2017-15112 unsafe use of -p/--admin-password on command line

It is unsafe to pass a password as part of the command line args
because it appears in the process info and is recorded in the
shell command history.

The -p --keycloak-admin-password arg has been replaced with -P
--keycloak-admin-password-file which reads the password from a file or
stdin if given a hyphen. The now deprecated --keycloak-admin-password
also accepted stdin with a hyphen, this was retained for backward
compatibility during a transition period, it will issue a deprecation
warning. Trying to use --keycloak-admin-password with anything other
than a hyphen will result in an error and explanation.

The man page was updated to include a section on password passing and
includes documention on using the KEYCLOAK_ADMIN_PASSWORD environment
variable (which was always supported but never documented).

The --admin-password command line arg in keycloak_cli.py was also
updated in a similar manner and now also includes support for
KEYCLOAK_ADMIN_PASSWORD environment variable.

Diff: diff --git a/bin/keycloak-httpd-client-install b/bin/keycloak-httpd-client-install
index abaaee0..950aeb1 100755
--- a/bin/keycloak-httpd-client-install
+++ b/bin/keycloak-httpd-client-install
@@ -850,10 +850,33 @@ def arg_type_mellon_protected_location(value):
     if not value.startswith('/'):
         raise argparse.ArgumentTypeError('Location must be absolute '
                                          '(arg="%s")' % value)
     return value.rstrip(" /")
 
+class DeprecatedPasswordAction(argparse.Action):
+    def __init__(self, option_strings, dest, nargs=None, **kwargs):
+        super(DeprecatedPasswordAction, self).__init__(option_strings, dest,
+                                                       **kwargs)
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        msg1 = ('%s Deprecated. Use --keycloak-admin-password-file instead '
+                '(which also accepts - to read from stdin) '
+                'or the KEYCLOAK_ADMIN_PASSWORD environment variable. '
+                'Using "-" to read the password from stdin '
+                'will continue to work for the new few releases for backward '
+                'compatibility, then will be removed. '
+                'See the man page.' % self.option_strings)
+        msg2 = ('Deprecated. It is insecure to pass a password on the '
+                'command line. Use --keycloak-admin-password-file or the '
+                'KEYCLOAK_ADMIN_PASSWORD environment variable instead. '
+                'See the man page.')
+        if values == '-':
+            print(msg1, file=sys.stderr)
+            setattr(namespace, self.dest, values)
+        else:
+            raise argparse.ArgumentError(self, msg2)
+
 # -----------------------------------------------------------------------------
 
 
 def main():
     global logger, template_env
@@ -939,12 +962,23 @@ def main():
                        '(default: root-admin)')
 
     group.add_argument('-u', '--keycloak-admin-username', default='admin',
                        help='admin user name (default: admin)')
 
+    group.add_argument('-P', '--keycloak-admin-password-file',
+                       type=argparse.FileType('rb'),
+                       help=('file containing admin password '
+                             '(or use a hyphen "-" to read the password '
+                             'from stdin)'))
+
     group.add_argument('-p', '--keycloak-admin-password',
-                       help='admin password (use - to read from stdin)')
+                       action=DeprecatedPasswordAction,
+                       dest='deprecated_keycloak_admin_password',
+                       help=('Deprecated. Use  --keycloak-admin-password-file '
+                             'instead. During transition perion this argument '
+                             'will continue to accept a hyphen to indicate '
+                             'the password will be read read from stdin)'))
 
     group.add_argument('--keycloak-admin-realm',
                        default='master',
                        help='realm admin belongs to')
 
@@ -1018,21 +1052,38 @@ def main():
 
     configure_logging(options)
 
     # ===== Options requiring special handling =====
 
+    options.keycloak_admin_password = None
     if options.keycloak_auth_role in ['root-admin', 'realm-admin']:
+
+        # We need the admin password for this role
+
+        # 1. Try password file
+        if options.keycloak_admin_password_file is not None:
+            options.keycloak_admin_password = options.keycloak_admin_password_file.readline().strip()
+            options.keycloak_admin_password_file.close()
+
+        # 2. Try KEYCLOAK_ADMIN_PASSWORD environment variable
         if options.keycloak_admin_password is None:
             if (('KEYCLOAK_ADMIN_PASSWORD' in os.environ) and
                 (os.environ['KEYCLOAK_ADMIN_PASSWORD'])):
                 options.keycloak_admin_password = (
                     os.environ['KEYCLOAK_ADMIN_PASSWORD'])
-            else:
-                options.keycloak_admin_password = getpass.getpass(
-                    '%s password: ' % (options.keycloak_admin_username))
-        elif options.keycloak_admin_password == '-':
-            options.keycloak_admin_password = sys.stdin.readline().rstrip('\n')
+
+        # 3. Try deprecated keycloak-admin-password, accept only hyphen for stdin
+        try:
+            if options.deprecated_keycloak_admin_password == '-':
+                options.keycloak_admin_password = sys.stdin.readline().strip()
+        except AttributeError:
+            pass
+
+        # 4. Try prompting for the password from the terminal
+        if options.keycloak_admin_password is None:
+            options.keycloak_admin_password = getpass.getpass(
+                'enter %s password: ' % (options.keycloak_admin_username))
 
         if not options.keycloak_admin_password:
             parser.error('argument %s is required '
                          'unless passed in the environment '
                          'variable KEYCLOAK_ADMIN_PASSWORD' %
diff --git a/doc/keycloak-httpd-client-install.8 b/doc/keycloak-httpd-client-install.8
index c3d0acd..cc1b33a 100644
--- a/doc/keycloak-httpd-client-install.8
+++ b/doc/keycloak-httpd-client-install.8
@@ -19,10 +19,11 @@ Tools to configure Apache HTTPD as Keycloak client
 [\fB\-\-template\-dir\fR \fITEMPLATE_DIR\fR]
 [\fB\-\-httpd\-dir\fR \fIHTTPD_DIR\fR] \-r KEYCLOAK_REALM
 \fB\-s\fR \fIKEYCLOAK_SERVER_URL\fR
 [\fB\-a\fR \fIroot\-admin|realm\-admin|anonymous\fR]
 [\fB\-u\fR \fIKEYCLOAK_ADMIN_USERNAME\fR]
+\fB\-P\fR \fIKEYCLOAK_ADMIN_PASSWORD_FILE\fR
 \fB\-p\fR \fIKEYCLOAK_ADMIN_PASSWORD\fR
 [\fB\-\-keycloak\-admin\-realm\fR \fIKEYCLOAK_ADMIN_REALM\fR]
 [\fB\-\-initial\-access\-token\fR \fIINITIAL_ACCESS_TOKEN\fR]
 [\fB\-\-client\-originate\-method\fR \fIdescriptor|registration\fR]
 [\fB\-\-mellon\-key\-file\fR \fIMELLON_KEY_FILE\fR]
@@ -114,13 +115,25 @@ authenticating as what type of user
 .TP
 .BR \-u ", " \-\-keycloak\-admin\-username " " \fIKEYCLOAK_ADMIN_USERNAME\fR
 admin user name
 (default: admin)
 .TP
+.BR \-P ", " \-\-keycloak\-admin\-password\-file " " \fIKEYCLOAK_ADMIN_PASSWORD_FILE\fR
+file containing the admin password (or use a hyphen "-" to indicate the
+password will be read from stdin)
+(default: None)
+.TP
 .BR \-p ", " \-\-keycloak\-admin\-password " " \fIKEYCLOAK_ADMIN_PASSWORD\fR
-admin password (use - to read from stdin)
+admin password (or use a hyphen "-" to to indicate the password will read
+from stdin)
 (default: None)
+\fB Deprecated.\fR It is insecure to pass a password on the command line.
+Use \fI\-\-keycloak\-admin\-password\-file\fR instead. \fBIt is no longer
+possible to use this argument to pass a password.\fR During a transition
+period this argument will continue to accept a hyphen to indicate the
+password should be read from stdin in a manner identical to
+\fI\-\-keycloak\-admin\-password\-file\fR.
 .TP
 .BR \-\-keycloak\-admin\-realm " " \fIKEYCLOAK_ADMIN_REALM\fR
 realm admin belongs to
 (default: master)
 .TP
@@ -184,10 +197,35 @@ Web location to protect with Mellon. May be specified multiple times
 
 .SH DESCRIPTION
 
 \fBkeycloak\-httpd\-client\-install\fR will configure a node running Apache with mod_auth_mellon as SAML Service Provider (\fBSP\fR) utilizing a \fBKeycloak\fR server as an Identity Provider(\fBIdP\fR).
 
+.PP
+.B How to pass the Keycloak admin password
+
+.PP
+The Keycloak admin password may be passed via one of the possible ways listed
+here in the order the tool looks for the password.
+
+.PP
+\fB1.\fR Try the \fI\-\-keycloak\-admin\-password\-file\fR argument.
+If it's a hyphen read the password from stdin, otherwise treat the argument
+as the name of a file, open the file and read the password from the file.
+
+.PP
+\fB2.\fR Test for the existence of the \fBKEYCLOAK_ADMIN_PASSWORD\fR
+environment variable. If the \fBKEYCLOAK_ADMIN_PASSWORD\fR is defined
+read the password from it.
+
+.PP
+\fB3.\fR Try the \fBdeprecated\fR \fI\-\-keycloak\-admin\-password\fR argument.
+If it's a hyphen read the password from stdin, otherwise return an error
+because passwords should never be passed on the command line.
+
+.PP
+\fB4.\fR Prompt for the password from the terminal.
+
 .PP
 .B Authentication Levels and Permissions
 
 .PP
 The tool is capable of range of configuration steps. But the extent of those operations may be circumscribed by the privilege level (authorization) the tool is run with. The privilege level is determined by the \fB\-\-keycloak\-auth\-role\fR command line option which may be one of:
@@ -279,11 +317,11 @@ The REST API may be used by the following authentication roles:
 
 .PP
 \fB*\fR Connect to Keycloak Server.
 .RS
 .PP
-A session is established with the Keycloak server. OAuth2 is used to log in as the admin user using the \fB\-\-keycloak\-admin\-username\fR and \fB\-\-keycloak\-admin\-password\fR options. The Keycloak server is identified by the \fB\-keycloak\-server\-url\fR option. This step is performed first to assure the remaining steps can complete successfully. A session is maintained for efficiency reasons. You may also need to specify \fB\-\-keycloak\-admin\-role\fR and \fB\-\-keycloak\-admin\-realm\fR to indicate the privilege level you are authenticating with. An anonymous auth role connects to the Keycloak service without any authentication.
+A session is established with the Keycloak server. OAuth2 is used to log in as the admin user using the \fB\-\-keycloak\-admin\-username\fR and \fB\-\-keycloak\-admin\-password\-file\fR options. The Keycloak server is identified by the \fB\-keycloak\-server\-url\fR option. This step is performed first to assure the remaining steps can complete successfully. A session is maintained for efficiency reasons. You may also need to specify \fB\-\-keycloak\-admin\-role\fR and \fB\-\-keycloak\-admin\-realm\fR to indicate the privilege level you are authenticating with. An anonymous auth role connects to the Keycloak service without any authentication.
 .RE
 
 .PP
 \fB*\fR Create directories.
 .RS
diff --git a/keycloak_httpd_client/keycloak_cli.py b/keycloak_httpd_client/keycloak_cli.py
index 876e0ca..3bd88f1 100755
--- a/keycloak_httpd_client/keycloak_cli.py
+++ b/keycloak_httpd_client/keycloak_cli.py
@@ -811,11 +811,11 @@ verb is an action to perform on the item. Each of the nouns and verbs
 may have their own set of arguments which must follow the noun or
 verb.
 
 For example to delete the client XYZ in the realm ABC:
 
-{prog_name} -s http://example.com:8080 -p password client delete -r ABC -c XYZ
+echo password | {prog_name} -s http://example.com:8080 -P - client delete -r ABC -c XYZ
 
 where 'client' is the noun, 'delete' is the verb and -r ABC -c XYZ are
 arguments to the delete action.
 
 If the command completes successfully the exit status is 0. The exit
@@ -893,13 +893,15 @@ def main():
 
     group.add_argument('-u', '--admin-username',
                        default='admin',
                        help='admin user name (default: admin)')
 
-    group.add_argument('-p', '--admin-password',
-                       required=True,
-                       help='admin password')
+    group.add_argument('-P', '--admin-password-file',
+                       type=argparse.FileType('rb'),
+                       help=('file containing admin password '
+                             '(or use a hyphen "-" to read the password '
+                             'from stdin)'))
 
     group.add_argument('--admin-realm',
                        default='master',
                        help='realm admin belongs to')
 
@@ -993,10 +995,24 @@ def main():
     configure_logging(options)
 
     if options.permit_insecure_transport:
         os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
 
+    # Get admin password
+    options.admin_password = None
+
+    # 1. Try password file
+    if options.admin_password_file is not None:
+        options.admin_password = options.keycloak_admin_password_file.readline().strip()
+        options.keycloak_admin_password_file.close()
+
+    # 2. Try KEYCLOAK_ADMIN_PASSWORD environment variable
+    if options.admin_password is None:
+        if (('KEYCLOAK_ADMIN_PASSWORD' in os.environ) and
+            (os.environ['KEYCLOAK_ADMIN_PASSWORD'])):
+            options.admin_password = os.environ['KEYCLOAK_ADMIN_PASSWORD']
+
     try:
         anonymous_conn = KeycloakAnonymousConnection(options.server,
                                                      options.tls_verify)
 
         admin_conn = KeycloakAdminConnection(options.server,

Vulnerable code:                        help='admin password (use - to read from stdin)')
            else:
                options.keycloak_admin_password = getpass.getpass(
                    '%s password: ' % (options.keycloak_admin_username))
        elif options.keycloak_admin_password == '-':
            options.keycloak_admin_password = sys.stdin.readline().rstrip('\n')
admin password (use - to read from stdin)
A session is established with the Keycloak server. OAuth2 is used to log in as the admin user using the \fB\-\-keycloak\-admin\-username\fR and \fB\-\-keycloak\-admin\-password\fR options. The Keycloak server is identified by the \fB\-keycloak\-server\-url\fR option. This step is performed first to assure the remaining steps can complete successfully. A session is maintained for efficiency reasons. You may also need to specify \fB\-\-keycloak\-admin\-role\fR and \fB\-\-keycloak\-admin\-realm\fR to indicate the privilege level you are authenticating with. An anonymous auth role connects to the Keycloak service without any authentication.
{prog_name} -s http://example.com:8080 -p password client delete -r ABC -c XYZ
    group.add_argument('-p', '--admin-password',
                       required=True,
                       help='admin password')
Fixed code: class DeprecatedPasswordAction(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        super(DeprecatedPasswordAction, self).__init__(option_strings, dest,
                                                       **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        msg1 = ('%s Deprecated. Use --keycloak-admin-password-file instead '
                '(which also accepts - to read from stdin) '
                'or the KEYCLOAK_ADMIN_PASSWORD environment variable. '
                'Using "-" to read the password from stdin '
                'will continue to work for the new few releases for backward '
                'compatibility, then will be removed. '
                'See the man page.' % self.option_strings)
        msg2 = ('Deprecated. It is insecure to pass a password on the '
                'command line. Use --keycloak-admin-password-file or the '
                'KEYCLOAK_ADMIN_PASSWORD environment variable instead. '
                'See the man page.')
        if values == '-':
            print(msg1, file=sys.stderr)
            setattr(namespace, self.dest, values)
        else:
            raise argparse.ArgumentError(self, msg2)

    group.add_argument('-P', '--keycloak-admin-password-file',
                       type=argparse.FileType('rb'),
                       help=('file containing admin password '
                             '(or use a hyphen "-" to read the password '
                             'from stdin)'))

                       action=DeprecatedPasswordAction,
                       dest='deprecated_keycloak_admin_password',
                       help=('Deprecated. Use  --keycloak-admin-password-file '
                             'instead. During transition perion this argument '
                             'will continue to accept a hyphen to indicate '
                             'the password will be read read from stdin)'))
    options.keycloak_admin_password = None

        # We need the admin password for this role

        # 1. Try password file
        if options.keycloak_admin_password_file is not None:
            options.keycloak_admin_password = options.keycloak_admin_password_file.readline().strip()
            options.keycloak_admin_password_file.close()

        # 2. Try KEYCLOAK_ADMIN_PASSWORD environment variable

        # 3. Try deprecated keycloak-admin-password, accept only hyphen for stdin
        try:
            if options.deprecated_keycloak_admin_password == '-':
                options.keycloak_admin_password = sys.stdin.readline().strip()
        except AttributeError:
            pass

        # 4. Try prompting for the password from the terminal
        if options.keycloak_admin_password is None:
            options.keycloak_admin_password = getpass.getpass(
                'enter %s password: ' % (options.keycloak_admin_username))
\fB\-P\fR \fIKEYCLOAK_ADMIN_PASSWORD_FILE\fR
.BR \-P ", " \-\-keycloak\-admin\-password\-file " " \fIKEYCLOAK_ADMIN_PASSWORD_FILE\fR
file containing the admin password (or use a hyphen "-" to indicate the
password will be read from stdin)
(default: None)
.TP
admin password (or use a hyphen "-" to to indicate the password will read
from stdin)
\fB Deprecated.\fR It is insecure to pass a password on the command line.
Use \fI\-\-keycloak\-admin\-password\-file\fR instead. \fBIt is no longer
possible to use this argument to pass a password.\fR During a transition
period this argument will continue to accept a hyphen to indicate the
password should be read from stdin in a manner identical to
\fI\-\-keycloak\-admin\-password\-file\fR.
.PP
.B How to pass the Keycloak admin password

.PP
The Keycloak admin password may be passed via one of the possible ways listed
here in the order the tool looks for the password.

.PP
\fB1.\fR Try the \fI\-\-keycloak\-admin\-password\-file\fR argument.
If it's a hyphen read the password from stdin, otherwise treat the argument
as the name of a file, open the file and read the password from the file.

.PP
\fB2.\fR Test for the existence of the \fBKEYCLOAK_ADMIN_PASSWORD\fR
environment variable. If the \fBKEYCLOAK_ADMIN_PASSWORD\fR is defined
read the password from it.

.PP
\fB3.\fR Try the \fBdeprecated\fR \fI\-\-keycloak\-admin\-password\fR argument.
If it's a hyphen read the password from stdin, otherwise return an error
because passwords should never be passed on the command line.

.PP
\fB4.\fR Prompt for the password from the terminal.

A session is established with the Keycloak server. OAuth2 is used to log in as the admin user using the \fB\-\-keycloak\-admin\-username\fR and \fB\-\-keycloak\-admin\-password\-file\fR options. The Keycloak server is identified by the \fB\-keycloak\-server\-url\fR option. This step is performed first to assure the remaining steps can complete successfully. A session is maintained for efficiency reasons. You may also need to specify \fB\-\-keycloak\-admin\-role\fR and \fB\-\-keycloak\-admin\-realm\fR to indicate the privilege level you are authenticating with. An anonymous auth role connects to the Keycloak service without any authentication.
echo password | {prog_name} -s http://example.com:8080 -P - client delete -r ABC -c XYZ
    group.add_argument('-P', '--admin-password-file',
                       type=argparse.FileType('rb'),
                       help=('file containing admin password '
                             '(or use a hyphen "-" to read the password '
                             'from stdin)'))
    # Get admin password
    options.admin_password = None

    # 1. Try password file
    if options.admin_password_file is not None:
        options.admin_password = options.keycloak_admin_password_file.readline().strip()
        options.keycloak_admin_password_file.close()

    # 2. Try KEYCLOAK_ADMIN_PASSWORD environment variable
    if options.admin_password is None:
        if (('KEYCLOAK_ADMIN_PASSWORD' in os.environ) and
            (os.environ['KEYCLOAK_ADMIN_PASSWORD'])):
            options.admin_password = os.environ['KEYCLOAK_ADMIN_PASSWORD']