Skip to content

Commit ef6da20

Browse files
lfospazz
authored andcommitted
Allow SGR ANSI control sequences
Keep ANSI escape sequences used for coloring and font formatting in the message body, while removing potentially dangerous sequences that may be used for escape sequence attacks. Fixes issue #1679.
1 parent b632616 commit ef6da20

2 files changed

Lines changed: 36 additions & 2 deletions

File tree

alot/helper.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,32 @@ def string_sanitize(string, tab_width=8):
6868
'foo bar'
6969
"""
7070

71-
string = ''.join([c for c in string if unicode_printable(c)])
71+
preprocessed_string = ''
72+
i = 0
73+
while i < len(string):
74+
# Check if this is the start of an allowed ANSI escape sequence. For
75+
# security reasons, we only allow SGR (Select Graphic Rendition)
76+
# parameters; i.e., sequences used for basic formatting such as
77+
# coloring and font variants.
78+
if string[i:i + 2] == '\x1b[':
79+
j = i + 2
80+
# Skip over all "parameter bytes" and "intermediate bytes"
81+
for skip_chars in ('0123456789:;<=>?', ' !"#$%&\'()*+,-./'):
82+
while j < len(string) and string[j] in skip_chars:
83+
j += 1
84+
# Keep only SGR sequences in the preprocessed string
85+
if j < len(string) and string[j] == 'm':
86+
preprocessed_string += string[i:j + 1]
87+
i = j + 1
88+
continue
89+
90+
# Not a valid ANSI escape sequence, only allow printable characters.
91+
if unicode_printable(string[i]):
92+
preprocessed_string += string[i]
93+
i += 1
7294

7395
lines = list()
74-
for line in string.split('\n'):
96+
for line in preprocessed_string.split('\n'):
7597
tab_count = line.count('\t')
7698

7799
if tab_count > 0:

tests/test_helper.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ def test_control_characters(self):
173173
actual = helper.string_sanitize(base)
174174
self.assertEqual(actual, expected)
175175

176+
def test_valid_ansi_escape_seq(self):
177+
base = 'foo\x1b[1;31mred\x1b[0mreset'
178+
expected = 'foo\x1b[1;31mred\x1b[0mreset'
179+
actual = helper.string_sanitize(base)
180+
self.assertEqual(actual, expected)
181+
182+
def test_invalid_ansi_escape_seq(self):
183+
base = 'cursor\x1b[2A manipulation'
184+
expected = 'cursor manipulation'
185+
actual = helper.string_sanitize(base)
186+
self.assertEqual(actual, expected)
187+
176188

177189
class TestStringDecode(unittest.TestCase):
178190

0 commit comments

Comments
 (0)