Skip to content

Commit 6eb8b4a

Browse files
authored
Update configure set to validate configuration keys and values (#10163)
* Implement newline and carriage-return sanitation for . * Implement regression tests to verify newline and return-carriage characters cannot be used in 'configure set'. * Add changelog entry. * Minor patch to command under test. * Fix tests so they write to disk properly. * Remove values from the error message, update tests.
1 parent 2baa4c8 commit 6eb8b4a

4 files changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Configuration",
4+
"description": "Update ``configure set`` command to return an error when newline or carriage-return characters are specified in the value."
5+
}

awscli/customizations/configure/writer.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ class ConfigFileWriter(object):
2424
r'(?P<value>.*)$'
2525
)
2626

27+
def _validate_no_newlines_or_carriage_returns(
28+
self,
29+
value,
30+
label='value',
31+
msg_override=None,
32+
):
33+
if isinstance(value, str) and ('\n' in value or '\r' in value):
34+
err_msg = msg_override if msg_override is not None else (
35+
f"Invalid {label}: newline "
36+
f"characters and carriage returns are not allowed: {value!r}"
37+
)
38+
raise ValueError(err_msg)
39+
2740
def update_config(self, new_values, config_filename):
2841
"""Update config file with new values.
2942
@@ -52,6 +65,38 @@ def update_config(self, new_values, config_filename):
5265
5366
"""
5467
section_name = new_values.pop('__section__', 'default')
68+
self._validate_no_newlines_or_carriage_returns(
69+
section_name,
70+
'section name'
71+
)
72+
for k, v in new_values.items():
73+
self._validate_no_newlines_or_carriage_returns(k, 'key')
74+
if not isinstance(v, dict):
75+
# Override error msg to prevent
76+
# leaking sensitive config values to stderr.
77+
self._validate_no_newlines_or_carriage_returns(
78+
v,
79+
'value',
80+
msg_override=(
81+
f"Invalid value for key {k}: "
82+
f"newline characters and carriage "
83+
f"returns are not allowed."
84+
)
85+
)
86+
else:
87+
for sk, sv in v.items():
88+
# Override error msg to prevent
89+
# leaking sensitive config values to stderr.
90+
self._validate_no_newlines_or_carriage_returns(sk, 'key')
91+
self._validate_no_newlines_or_carriage_returns(
92+
sv,
93+
'value',
94+
msg_override = (
95+
f"Invalid value for key {k}: "
96+
f"newline characters and carriage "
97+
f"returns are not allowed."
98+
)
99+
)
55100
if not os.path.isfile(config_filename):
56101
self._create_file(config_filename)
57102
self._write_new_section(section_name, new_values, config_filename)

tests/functional/configure/test_configure.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,62 @@ def test_can_handle_empty_section(self):
374374
self.get_config_file_contents(),
375375
)
376376

377+
def test_set_rejects_newline_in_value(self):
378+
_, stderr, _ = self.run_cmd(
379+
["configure", "set", "region", "us-east-1\nus-west-2"],
380+
expected_rc=255,
381+
)
382+
self.assertIn("newline", stderr)
383+
# To avoid leaking sensitive values,
384+
# values should not appear in stderr.
385+
self.assertNotIn("us-east-1\nus-west-2", stderr)
386+
387+
def test_set_rejects_carriage_return_in_value(self):
388+
_, stderr, _ = self.run_cmd(
389+
["configure", "set", "region", "us-east-1\rus-west-2"],
390+
expected_rc=255,
391+
)
392+
self.assertIn("newline", stderr)
393+
# To avoid leaking sensitive values,
394+
# values should not appear in stderr.
395+
self.assertNotIn("us-east-1\rus-west-2", stderr)
396+
397+
def test_set_rejects_newline_in_nested_value(self):
398+
_, stderr, _ = self.run_cmd(
399+
["configure", "set", "default.s3.signature_version", "s3v4\nfoo"],
400+
expected_rc=255,
401+
)
402+
self.assertIn("newline", stderr)
403+
# To avoid leaking sensitive values,
404+
# values should not appear in stderr.
405+
self.assertNotIn("s3v4\nfoo", stderr)
406+
407+
def test_newline_injection_does_not_write_injected_key_to_file(self):
408+
# Simulates: aws configure set output $'table\nregion = us-east-1'
409+
# The injected key must not appear anywhere in the config file.
410+
self.set_config_file_contents("[default]\n")
411+
self.run_cmd(
412+
["configure", "set", "output", "table\nregion = us-east-1"],
413+
expected_rc=255,
414+
)
415+
contents = self.get_config_file_contents()
416+
self.assertNotIn("region", contents)
417+
418+
def test_newline_injection_does_not_set_injected_key_in_parsed_config(self):
419+
# Even if the file were somehow written, the injected key must not be
420+
# readable back via 'configure get'.
421+
self.set_config_file_contents("[default]\n")
422+
self.run_cmd(
423+
["configure", "set", "output", "table\nregion = us-east-1"],
424+
expected_rc=255,
425+
)
426+
# Re-create the driver so it re-reads the (unchanged) config file.
427+
self.driver = create_clidriver()
428+
stdout, _, _ = self.run_cmd(
429+
"configure get region", expected_rc=1
430+
)
431+
self.assertEqual(stdout.strip(), "")
432+
377433

378434
class TestConfigureHasArgTable(unittest.TestCase):
379435
def test_configure_command_has_arg_table(self):

tests/unit/customizations/configure/test_writer.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,39 @@ def test_appends_newline_on_new_section(self):
369369
'[new-section]\n'
370370
'region = us-west-2\n'
371371
)
372+
373+
def test_newline_in_value_raises(self):
374+
with open(self.config_filename, 'w') as f:
375+
f.write('[default]\nfoo = bar\n')
376+
with self.assertRaises(ValueError):
377+
self.writer.update_config({'foo': 'bad\nvalue'}, self.config_filename)
378+
379+
def test_carriage_return_in_value_raises(self):
380+
with open(self.config_filename, 'w') as f:
381+
f.write('[default]\nfoo = bar\n')
382+
with self.assertRaises(ValueError):
383+
self.writer.update_config({'foo': 'bad\rvalue'}, self.config_filename)
384+
385+
def test_newline_in_key_raises(self):
386+
with open(self.config_filename, 'w') as f:
387+
f.write('[default]\nfoo = bar\n')
388+
with self.assertRaises(ValueError):
389+
self.writer.update_config({'bad\nkey': 'value'}, self.config_filename)
390+
391+
def test_newline_in_section_name_raises(self):
392+
with open(self.config_filename, 'w') as f:
393+
f.write('[default]\nfoo = bar\n')
394+
with self.assertRaises(ValueError):
395+
self.writer.update_config(
396+
{'foo': 'value', '__section__': 'bad\nsection'},
397+
self.config_filename
398+
)
399+
400+
def test_newline_in_nested_value_raises(self):
401+
with open(self.config_filename, 'w') as f:
402+
f.write('[default]\n')
403+
with self.assertRaises(ValueError):
404+
self.writer.update_config(
405+
{'__section__': 'default', 's3': {'key': 'bad\nvalue'}},
406+
self.config_filename
407+
)

0 commit comments

Comments
 (0)