Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further Sanitize User Input #425

Merged
merged 8 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.5.5
6.5.6
71 changes: 58 additions & 13 deletions confidant/routes/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,9 +594,8 @@ def create_credential():
'''
with stats.timer('create_credential'):
if not acl_module_check(resource_type='credential', action='create'):
msg = "{} does not have access to create credentials".format(
authnz.get_logged_in_user()
)
msg = f"{authnz.get_logged_in_user()}"
msg += "does not have access to create credentials"
error_msg = {'error': msg}
return jsonify(error_msg), 403

Expand All @@ -621,7 +620,7 @@ def create_credential():
for cred in Credential.data_type_date_index.query(
'credential', filter_condition=Credential.name == data['name']):
# Conflict, the name already exists
msg = 'Name already exists. See id: {0}'.format(cred.id)
msg = f'Name already exists. See id: {cred.id}'
return jsonify({'error': msg, 'reference': cred.id}), 409
# Generate an initial stable ID to allow name changes
id = str(uuid.uuid4()).replace('-', '')
Expand All @@ -636,13 +635,20 @@ def create_credential():
credential_pairs = cipher.encrypt(credential_pairs)
last_rotation_date = misc.utcnow()

metadata = data.get('metadata', {})
for key, value in metadata.items():
value = escape(value)
metadata[key] = value

data['documentation'] = escape(data.get('documentation'))

sanitized_name = escape(data['name'])
cred = Credential(
id='{0}-{1}'.format(id, revision),
id=f'{id}-{revision}',
data_type='archive-credential',
name=sanitized_name,
credential_pairs=credential_pairs,
metadata=data.get('metadata'),
metadata=metadata,
revision=revision,
enabled=data.get('enabled'),
data_key=data_key['ciphertext'],
Expand Down Expand Up @@ -797,10 +803,8 @@ def update_credential(id):
if not acl_module_check(resource_type='credential',
action='update',
resource_id=id):
msg = "{} does not have access to update credential {}".format(
authnz.get_logged_in_user(),
id
)
msg = f"{authnz.get_logged_in_user()}"
msg += f"does not have access to update credential {id}"
error_msg = {'error': msg, 'reference': id}
return jsonify(error_msg), 403

Expand All @@ -816,6 +820,34 @@ def update_credential(id):
if not isinstance(data.get('metadata', {}), dict):
return jsonify({'error': 'metadata must be a dict'}), 400

# We check for a name change and ensure it doesn't conflict with an
# existing credential and to ensure we don't escape the name if it
# hasn't changed
if data.get('name') != _cred.name:
data['name'] = escape(data.get('name'))
for cred in Credential.data_type_date_index.query(
'credential',
filter_condition=Credential.name == data['name']):
# Conflict, the name already exists
msg = f'Name already exists. See id: {cred.id}'
return jsonify({'error': msg, 'reference': cred.id}), 409
Comment on lines +826 to +833
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the latency here? i remember you mentioned a high latency for check dup

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the whole dup check adds more latency since we need to decrypt credential pairs to verify if the cred is a full duplicate. this check is only for dup name, which is not as expensive


# Escape metadata values by checking for new metadata keys and values
# to ensure we don't escape values that haven't changed
if data.get('metadata') != _cred.metadata:
new_metadata = {
key: value
for key, value in data.get('metadata', {}).items()
if key not in _cred.metadata or
value != _cred.metadata.get(key)
}
for key, value in new_metadata.items():
value = escape(value)
data['metadata'][key] = value

if data.get('documentation') != _cred.documentation:
data['documentation'] = escape(data.get('documentation'))

update = {
'name': data.get('name', _cred.name),
'last_rotation_date': _cred.last_rotation_date,
Expand Down Expand Up @@ -874,6 +906,19 @@ def update_credential(id):
# this is a new credential pair and update last_rotation_date
if credential_pairs != _cred.decrypted_credential_pairs:
update['last_rotation_date'] = misc.utcnow()

# We escape credential pairs by checking for new credential
# pairs and values to ensure we don't escape values that haven't
# changed
new_credential_pairs = {
key: value
for key, value in credential_pairs.items()
if key not in _cred.decrypted_credential_pairs or
value != _cred.decrypted_credential_pairs.get(key)
}
for key, value in new_credential_pairs.items():
value = escape(value)
credential_pairs[key] = value
data_key = keymanager.create_datakey(encryption_context={'id': id})
cipher = CipherManager(data_key['plaintext'], version=2)
update['credential_pairs'] = cipher.encrypt(
Expand All @@ -883,7 +928,7 @@ def update_credential(id):
# Try to save to the archive
try:
Credential(
id='{0}-{1}'.format(id, revision),
id=f'{id}-{revision}',
name=update['name'],
data_type='archive-credential',
credential_pairs=update['credential_pairs'],
Expand Down Expand Up @@ -925,8 +970,8 @@ def update_credential(id):

if services:
service_names = [x.id for x in services]
msg = 'Updated credential "{0}" ({1}); Revision {2}'
msg = msg.format(cred.name, cred.id, cred.revision)
msg = f'Updated credential "{cred.name}"'
msg += f'({cred.id}); Revision {cred.revision}'
graphite.send_event(service_names, msg)
webhook.send_event('credential_update', service_names, [cred.id])
permissions = {
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/confidant/routes/credentials_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,11 @@ def test_update_credential(mocker: MockerFixture, credential: Credential):
'confidant.routes.credentials.Credential.get',
return_value=credential,
)
mocker.patch(
('confidant.routes.credentials.Credential'
'.data_type_date_index.query'),
return_value=[],
)
mocker.patch(
('confidant.routes.credentials.credentialmanager'
'.get_latest_credential_revision'),
Expand Down Expand Up @@ -567,7 +572,30 @@ def test_update_credential(mocker: MockerFixture, credential: Credential):
assert ret.status_code == 400
assert 'Credential Pairs cannot be empty.' == json_data['error']

# Credential name already exists (ie: query returns a value)
mocker.patch(
'confidant.routes.credentials.Credential.data_type_date_index.query',
return_value=[credential],
)
ret = app.test_client().put(
'/v1/credentials/123',
headers={"Content-Type": 'application/json'},
data=json.dumps({
'name': 'me',
'documentation': 'doc',
'credential_pairs': {'key': 'value'},
}),
)
json_data = json.loads(ret.data)
assert ret.status_code == 409
assert 'Name already exists' in json_data['error']

# All good
mocker.patch(
('confidant.routes.credentials.Credential'
'.data_type_date_index.query'),
return_value=[],
)
mocker.patch(
('confidant.routes.credentials.servicemanager'
'.pair_key_conflicts_for_services'),
Expand Down
Loading