Skip to content

Commit 623ce0a

Browse files
Merge pull request #59 from aws-samples/kyboxethdev
Escape user input and add additional security checks
2 parents cff0165 + da11a02 commit 623ce0a

File tree

14 files changed

+618
-128
lines changed

14 files changed

+618
-128
lines changed

SecretsManagerMongoDBRotationMultiUser/lambda_function.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,26 @@ def set_secret(service_client, arn, token):
154154
155155
"""
156156
# First try to login with the pending secret, if it succeeds, return
157+
current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
157158
pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token)
158159
conn = get_connection(pending_dict)
159160
if conn:
160161
conn.logout()
161162
logger.info("setSecret: AWSPENDING secret is already set as password in MongoDB for secret arn %s." % arn)
162163
return
163164

165+
# Make sure the user from current and pending match
166+
if get_alt_username(current_dict['username']) != pending_dict['username']:
167+
logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username']))
168+
raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username']))
169+
170+
# Make sure the host from current and pending match
171+
if current_dict['host'] != pending_dict['host']:
172+
logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
173+
raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
174+
164175
# Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db
165176
# This ensures that the credential we are rotating is valid to protect against a confused deputy attack
166-
current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
167177
conn = get_connection(current_dict)
168178
if not conn:
169179
logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn)
@@ -174,7 +184,8 @@ def set_secret(service_client, arn, token):
174184
master_arn = current_dict['masterarn']
175185
master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT")
176186
if current_dict['host'] != master_dict['host']:
177-
logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host']))
187+
logger.error("setSecret: Current database host %s is not the same host as master %s" % (current_dict['host'], master_dict['host']))
188+
raise ValueError("Current database host %s is not the same host as master %s" % (current_dict['host'], master_dict['host']))
178189

179190
# Now log into the database with the master credentials
180191
conn = get_connection(master_dict)
@@ -191,6 +202,9 @@ def set_secret(service_client, arn, token):
191202
user_info = conn.command('usersInfo', current_dict['username'])
192203
conn.command("createUser", pending_dict['username'], pwd=pending_dict['password'], roles=user_info['users'][0]['roles'])
193204
logger.info("setSecret: Successfully set password for %s in MongoDB for secret arn %s." % (pending_dict['username'], arn))
205+
except errors.PyMongoError:
206+
logger.error("setSecret: Error encountered when attempting to set password in database for user %s", pending_dict['username'])
207+
raise ValueError("Error encountered when attempting to set password in database for user %s", pending_dict['username'])
194208
finally:
195209
conn.logout()
196210

@@ -288,9 +302,9 @@ def get_connection(secret_dict):
288302
ssl = False
289303
if 'ssl' in secret_dict:
290304
if type(secret_dict['ssl']) is bool:
291-
ssl = secret_dict['ssl']
292-
else:
293-
ssl = (secret_dict['ssl'].lower() == "true")
305+
ssl = secret_dict['ssl']
306+
else:
307+
ssl = (secret_dict['ssl'].lower() == "true")
294308

295309
# Try to obtain a connection to the db
296310
try:

SecretsManagerMongoDBRotationSingleUser/lambda_function.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,43 @@ def set_secret(service_client, arn, token):
146146
KeyError: If the secret json does not contain the expected keys
147147
148148
"""
149-
# First try to login with the pending secret, if it succeeds, return
149+
try:
150+
previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS")
151+
except (service_client.exceptions.ResourceNotFoundException, KeyError):
152+
previous_dict = None
153+
current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
150154
pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token)
155+
156+
# First try to login with the pending secret, if it succeeds, return
151157
conn = get_connection(pending_dict)
152158
if conn:
153159
conn.logout()
154160
logger.info("setSecret: AWSPENDING secret is already set as password in MongoDB for secret arn %s." % arn)
155161
return
156162

163+
# Make sure the user from current and pending match
164+
if current_dict['username'] != pending_dict['username']:
165+
logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username']))
166+
raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username']))
167+
168+
# Make sure the host from current and pending match
169+
if current_dict['host'] != pending_dict['host']:
170+
logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
171+
raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
172+
157173
# Now try the current password
158-
conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT"))
159-
if not conn:
174+
conn = get_connection(current_dict)
175+
if not conn and previous_dict:
160176
# If both current and pending do not work, try previous
161-
try:
162-
conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS"))
163-
except service_client.exceptions.ResourceNotFoundException:
164-
conn = None
177+
conn = get_connection(previous_dict)
178+
179+
# Make sure the user/host from previous and pending match
180+
if previous_dict['username'] != pending_dict['username']:
181+
logger.error("setSecret: Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username']))
182+
raise ValueError("Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username']))
183+
if previous_dict['host'] != pending_dict['host']:
184+
logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host']))
185+
raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host']))
165186

166187
# If we still don't have a connection, raise a ValueError
167188
if not conn:
@@ -172,6 +193,9 @@ def set_secret(service_client, arn, token):
172193
try:
173194
conn.command("updateUser", pending_dict['username'], pwd=pending_dict['password'])
174195
logger.info("setSecret: Successfully set password for user %s in MongoDB for secret arn %s." % (pending_dict['username'], arn))
196+
except errors.PyMongoError:
197+
logger.error("setSecret: Error encountered when attempting to set password in database for user %s", pending_dict['username'])
198+
raise ValueError("Error encountered when attempting to set password in database for user %s", pending_dict['username'])
175199
finally:
176200
conn.logout()
177201

@@ -267,10 +291,10 @@ def get_connection(secret_dict):
267291
ssl = False
268292
if 'ssl' in secret_dict:
269293
if type(secret_dict['ssl']) is bool:
270-
ssl = secret_dict['ssl']
271-
else:
272-
ssl = (secret_dict['ssl'].lower() == "true")
273-
294+
ssl = secret_dict['ssl']
295+
else:
296+
ssl = (secret_dict['ssl'].lower() == "true")
297+
274298
# Try to obtain a connection to the db
275299
try:
276300
client = MongoClient(host=secret_dict['host'], port=port, connectTimeoutMS=5000, serverSelectionTimeoutMS=5000, ssl=ssl)

SecretsManagerRDSMariaDBRotationMultiUser/lambda_function.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,28 @@ def set_secret(service_client, arn, token):
152152
KeyError: If the secret json does not contain the expected keys
153153
154154
"""
155-
# First try to login with the pending secret, if it succeeds, return
155+
current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
156156
pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token)
157+
158+
# First try to login with the pending secret, if it succeeds, return
157159
conn = get_connection(pending_dict)
158160
if conn:
159161
conn.close()
160162
logger.info("setSecret: AWSPENDING secret is already set as password in MariaDB DB for secret arn %s." % arn)
161163
return
162164

165+
# Make sure the user from current and pending match
166+
if get_alt_username(current_dict['username']) != pending_dict['username']:
167+
logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username']))
168+
raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username']))
169+
170+
# Make sure the host from current and pending match
171+
if current_dict['host'] != pending_dict['host']:
172+
logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
173+
raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
174+
163175
# Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db
164176
# This ensures that the credential we are rotating is valid to protect against a confused deputy attack
165-
current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
166177
conn = get_connection(current_dict)
167178
if not conn:
168179
logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn)
@@ -172,8 +183,10 @@ def set_secret(service_client, arn, token):
172183
# Now get the master arn from the current secret
173184
master_arn = current_dict['masterarn']
174185
master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT")
175-
if current_dict['host'] != master_dict['host']:
176-
logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host']))
186+
if current_dict['host'] != master_dict['host'] and not is_rds_replica_database(current_dict, master_dict):
187+
# If current dict is a replica of the master dict, can proceed
188+
logger.error("setSecret: Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host']))
189+
raise ValueError("Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host']))
177190

178191
# Now log into the database with the master credentials
179192
conn = get_connection(master_dict)
@@ -189,9 +202,8 @@ def set_secret(service_client, arn, token):
189202
cur.execute("SHOW GRANTS FOR %s", current_dict['username'])
190203
for row in cur.fetchall():
191204
grant = row[0].split(' TO ')
192-
new_grant = "%s TO %s" % (grant[0], pending_dict['username'])
193-
new_grant_escaped = new_grant.replace('%','%%') # % is a special character in Python format strings.
194-
cur.execute(new_grant_escaped + " IDENTIFIED BY %s", pending_dict['password'])
205+
new_grant_escaped = grant[0].replace('%','%%') # % is a special character in Python format strings.
206+
cur.execute(new_grant_escaped + " TO %s IDENTIFIED BY %s", (pending_dict['username'], pending_dict['password']))
195207
conn.commit()
196208
logger.info("setSecret: Successfully set password for %s in MariaDB DB for secret arn %s." % (pending_dict['username'], arn))
197209
finally:
@@ -365,3 +377,42 @@ def get_alt_username(current_username):
365377
if len(new_username) > 80:
366378
raise ValueError("Unable to clone user, username length with _clone appended would exceed 80 characters")
367379
return new_username
380+
381+
def is_rds_replica_database(replica_dict, master_dict):
382+
"""Validates that the database of a secret is a replica of the database of the master secret
383+
384+
This helper function validates that the database of a secret is a replica of the database of the master secret.
385+
386+
Args:
387+
replica_dict (dictionary): The secret dictionary containing the replica database
388+
389+
primary_dict (dictionary): The secret dictionary containing the primary database
390+
391+
Returns:
392+
isReplica : whether or not the database is a replica
393+
394+
Raises:
395+
ValueError: If the new username length would exceed the maximum allowed
396+
"""
397+
# Setup the client
398+
rds_client = boto3.client('rds')
399+
400+
# Get instance identifiers from endpoints
401+
replica_instance_id = replica_dict['host'].split(".")[0]
402+
master_instance_id = master_dict['host'].split(".")[0]
403+
404+
try:
405+
describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=replica_instance_id)
406+
except Exception as err:
407+
logger.warn("Encountered error while verifying rds replica status: %s" % err)
408+
return False
409+
instances = describe_response['DBInstances']
410+
411+
# Host from current secret cannot be found
412+
if not instances:
413+
logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id)
414+
return False
415+
416+
# DB Instance identifiers are unique - can only be one result
417+
current_instance = instances[0]
418+
return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier')

SecretsManagerRDSMariaDBRotationSingleUser/lambda_function.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,43 @@ def set_secret(service_client, arn, token):
145145
KeyError: If the secret json does not contain the expected keys
146146
147147
"""
148-
# First try to login with the pending secret, if it succeeds, return
148+
try:
149+
previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS")
150+
except (service_client.exceptions.ResourceNotFoundException, KeyError):
151+
previous_dict = None
152+
current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
149153
pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token)
154+
155+
# First try to login with the pending secret, if it succeeds, return
150156
conn = get_connection(pending_dict)
151157
if conn:
152158
conn.close()
153159
logger.info("setSecret: AWSPENDING secret is already set as password in MariaDB DB for secret arn %s." % arn)
154160
return
155161

162+
# Make sure the user from current and pending match
163+
if current_dict['username'] != pending_dict['username']:
164+
logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username']))
165+
raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username']))
166+
167+
# Make sure the host from current and pending match
168+
if current_dict['host'] != pending_dict['host']:
169+
logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
170+
raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host']))
171+
156172
# Now try the current password
157-
conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT"))
158-
if not conn:
173+
conn = get_connection(current_dict)
174+
if not conn and previous_dict:
159175
# If both current and pending do not work, try previous
160-
try:
161-
conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS"))
162-
except service_client.exceptions.ResourceNotFoundException:
163-
conn = None
176+
conn = get_connection(previous_dict)
177+
178+
# Make sure the user and host from previous and pending match
179+
if previous_dict['username'] != pending_dict['username']:
180+
logger.error("setSecret: Attempting to modify user %s other than last valid user %s" % (pending_dict['username'], previous_dict['username']))
181+
raise ValueError("Attempting to modify user %s other than last valid user %s" % (pending_dict['username'], previous_dict['username']))
182+
if previous_dict['host'] != pending_dict['host']:
183+
logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host']))
184+
raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host']))
164185

165186
# If we still don't have a connection, raise a ValueError
166187
if not conn:

0 commit comments

Comments
 (0)