Skip to content

Commit b6cde12

Browse files
authored
[v2] Update configure set to validate configuration keys and values (#10167)
* Add changelog entry. * Implement newline and carriage return sanitation for 'configure set' command. * Port removal of values in the stderr and updated tests.
1 parent a3fe024 commit b6cde12

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:
2424
r'(?P<value>.*)$'
2525
) # fmt: skip
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
@@ -299,6 +299,62 @@ def test_get_nested_attribute(self):
299299
)
300300
self.assertEqual(stdout, "")
301301

302+
def test_set_rejects_newline_in_value(self):
303+
_, stderr, _ = self.run_cmd(
304+
["configure", "set", "region", "us-east-1\nus-west-2"],
305+
expected_rc=255,
306+
)
307+
self.assertIn("newline", stderr)
308+
# To avoid leaking sensitive values,
309+
# values should not appear in stderr.
310+
self.assertNotIn("us-east-1\nus-west-2", stderr)
311+
312+
def test_set_rejects_carriage_return_in_value(self):
313+
_, stderr, _ = self.run_cmd(
314+
["configure", "set", "region", "us-east-1\rus-west-2"],
315+
expected_rc=255,
316+
)
317+
self.assertIn("newline", stderr)
318+
# To avoid leaking sensitive values,
319+
# values should not appear in stderr.
320+
self.assertNotIn("us-east-1\rus-west-2", stderr)
321+
322+
def test_set_rejects_newline_in_nested_value(self):
323+
_, stderr, _ = self.run_cmd(
324+
["configure", "set", "default.s3.signature_version", "s3v4\nfoo"],
325+
expected_rc=255,
326+
)
327+
self.assertIn("newline", stderr)
328+
# To avoid leaking sensitive values,
329+
# values should not appear in stderr.
330+
self.assertNotIn("s3v4\nfoo", stderr)
331+
332+
def test_newline_injection_does_not_write_injected_key_to_file(self):
333+
# Simulates: aws configure set output $'table\nregion = us-east-1'
334+
# The injected key must not appear anywhere in the config file.
335+
self.set_config_file_contents("[default]\n")
336+
self.run_cmd(
337+
["configure", "set", "output", "table\nregion = us-east-1"],
338+
expected_rc=255,
339+
)
340+
contents = self.get_config_file_contents()
341+
self.assertNotIn("region", contents)
342+
343+
def test_newline_injection_does_not_set_injected_key_in_parsed_config(self):
344+
# Even if the file were somehow written, the injected key must not be
345+
# readable back via 'configure get'.
346+
self.set_config_file_contents("[default]\n")
347+
self.run_cmd(
348+
["configure", "set", "output", "table\nregion = us-east-1"],
349+
expected_rc=255,
350+
)
351+
# Re-create the driver so it re-reads the (unchanged) config file.
352+
self.driver = create_clidriver()
353+
stdout, _, _ = self.run_cmd(
354+
"configure get region", expected_rc=1
355+
)
356+
self.assertEqual(stdout.strip(), "")
357+
302358

303359
class TestConfigureHasArgTable(unittest.TestCase):
304360
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
@@ -314,3 +314,39 @@ def test_appends_newline_on_new_section(self):
314314
'[new-section]\n'
315315
'region = us-west-2\n',
316316
)
317+
318+
def test_newline_in_value_raises(self):
319+
with open(self.config_filename, 'w') as f:
320+
f.write('[default]\nfoo = bar\n')
321+
with self.assertRaises(ValueError):
322+
self.writer.update_config({'foo': 'bad\nvalue'}, self.config_filename)
323+
324+
def test_carriage_return_in_value_raises(self):
325+
with open(self.config_filename, 'w') as f:
326+
f.write('[default]\nfoo = bar\n')
327+
with self.assertRaises(ValueError):
328+
self.writer.update_config({'foo': 'bad\rvalue'}, self.config_filename)
329+
330+
def test_newline_in_key_raises(self):
331+
with open(self.config_filename, 'w') as f:
332+
f.write('[default]\nfoo = bar\n')
333+
with self.assertRaises(ValueError):
334+
self.writer.update_config({'bad\nkey': 'value'}, self.config_filename)
335+
336+
def test_newline_in_section_name_raises(self):
337+
with open(self.config_filename, 'w') as f:
338+
f.write('[default]\nfoo = bar\n')
339+
with self.assertRaises(ValueError):
340+
self.writer.update_config(
341+
{'foo': 'value', '__section__': 'bad\nsection'},
342+
self.config_filename
343+
)
344+
345+
def test_newline_in_nested_value_raises(self):
346+
with open(self.config_filename, 'w') as f:
347+
f.write('[default]\n')
348+
with self.assertRaises(ValueError):
349+
self.writer.update_config(
350+
{'__section__': 'default', 's3': {'key': 'bad\nvalue'}},
351+
self.config_filename
352+
)

0 commit comments

Comments
 (0)