Skip to content

Commit 21a0544

Browse files
Cyrille BougotCyrilleB79
authored andcommitted
Fixes security issue GHSA-39pg-6xpm-mjgf
1 parent 3e53740 commit 21a0544

File tree

1 file changed

+125
-13
lines changed

1 file changed

+125
-13
lines changed

addon/globalPlugins/ndtt/logReader.py

Lines changed: 125 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818
from keyLabels import localizedKeyLabels
1919
import speech
2020
try:
21-
from speech.commands import ( # noqa: F401 - may be used in the evaluated speech sequence
21+
from speech.commands import (
2222
CallbackCommand,
2323
BeepCommand,
2424
ConfigProfileTriggerCommand,
2525
)
26-
from speech.commands import ( # noqa: F401 - may be used in the evaluated speech sequence
26+
from speech.commands import (
2727
CharacterModeCommand,
2828
LangChangeCommand,
2929
BreakCommand,
@@ -37,19 +37,14 @@
3737
preSpeechRefactor = False
3838
except ImportError:
3939
# NVDA <= 2019.2.1
40-
from speech import ( # noqa: F401 - may be used in the evaluated speech sequence
40+
from speech import (
4141
CharacterModeCommand,
4242
LangChangeCommand,
4343
BreakCommand,
44-
# EndUtteranceCommand,
4544
PitchCommand,
4645
VolumeCommand,
4746
RateCommand,
4847
PhonemeCommand,
49-
# CallbackCommand,
50-
# BeepCommand,
51-
# WaveFileCommand,
52-
# ConfigProfileTriggerCommand,
5348
)
5449
preSpeechRefactor = True
5550
from logHandler import log
@@ -75,6 +70,8 @@
7570

7671
import re
7772
import os
73+
import sys
74+
import ast
7875

7976

8077
addonHandler.initTranslation()
@@ -157,6 +154,118 @@ def noFilter(msg):
157154
return True
158155

159156

157+
cmdList = []
158+
if not preSpeechRefactor:
159+
# We ignore CallbackCommand and ConfigProfileTriggerCommand to avoid producing errors or unexpected
160+
# side effects.
161+
cmdList.append(CallbackCommand)
162+
cmdList.append(ConfigProfileTriggerCommand)
163+
FORBIDDEN_COMMANDS = {c.__name__ for c in cmdList}
164+
165+
cmdList = [
166+
CharacterModeCommand,
167+
LangChangeCommand,
168+
BreakCommand,
169+
PitchCommand,
170+
VolumeCommand,
171+
RateCommand,
172+
PhonemeCommand,
173+
]
174+
if not preSpeechRefactor:
175+
cmdList.extend([
176+
BeepCommand,
177+
EndUtteranceCommand,
178+
WaveFileCommand,
179+
])
180+
ALLOWED_COMMANDS = {c.__name__: c for c in cmdList}
181+
182+
183+
def astNodeToStr(node):
184+
# NVDA >= 2024.1 (using Python 3.11 or higher)
185+
if sys.version_info >= (3, 8):
186+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
187+
return node.value
188+
# NVDA < 2024.1 (using Python 2.7 or 3.7)
189+
else:
190+
if isinstance(node, ast.Str):
191+
return node.s
192+
raise ValueError("Node is not a string")
193+
194+
195+
def astNodeToLitteral(node):
196+
# NVDA >= 2024.1 (using Python 3.11 or higher)
197+
if sys.version_info >= (3, 8):
198+
if isinstance(node, ast.Constant):
199+
return node.value
200+
# NVDA < 2024.1 (using Python 2.7 or 3.7)
201+
else:
202+
if isinstance(node, ast.Str):
203+
return node.s
204+
if isinstance(node, ast.Num):
205+
return node.n
206+
if isinstance(node, ast.NameConstant):
207+
return node.value
208+
raise ValueError("Node is not a litteral")
209+
210+
def generateSpeechSequence(txtSeq):
211+
"""Generates a speech sequence from its representation in the log.
212+
Irrelevant commands are filtered out (CallbackCommand, _CancellableSpeechCommand)
213+
We do not use "eval" to avoid executing arbitrary code from a crafted log.
214+
"""
215+
216+
tree = ast.parse(txtSeq, mode="eval")
217+
if not isinstance(tree.body, ast.List):
218+
raise ValueError("Speech sequence must be a list")
219+
seq = []
220+
for elt in tree.body.elts:
221+
try:
222+
strVal = astNodeToStr(elt)
223+
except ValueError:
224+
pass
225+
else:
226+
seq.append(strVal)
227+
continue
228+
if isinstance(elt, ast.Call) and isinstance(elt.func, ast.Name):
229+
name = elt.func.id
230+
if name in FORBIDDEN_COMMANDS:
231+
continue
232+
if name not in ALLOWED_COMMANDS:
233+
log.error("Unsupported command: {name}".format(name=name))
234+
continue
235+
args = []
236+
valid = False
237+
for arg in elt.args:
238+
try:
239+
litteralVal = astNodeToLitteral(arg)
240+
except ValueError:
241+
log.error("Non-literal argument in {name}".format(name=name))
242+
break
243+
args.append(litteralVal)
244+
else:
245+
valid = True
246+
if not valid:
247+
continue
248+
249+
kwargs = {}
250+
valid = False
251+
for kw in elt.keywords:
252+
try:
253+
litteralVal = astNodeToLitteral(kw.value)
254+
except ValueError:
255+
log.error("Non-literal kwarg in {name}".format(name=name))
256+
break
257+
kwargs[kw.arg] = litteralVal
258+
else:
259+
valid = True
260+
if not valid:
261+
continue
262+
cmd_cls = ALLOWED_COMMANDS[name]
263+
seq.append(cmd_cls(*args, **kwargs))
264+
continue
265+
log.error("Unsupported AST node: {node}".format(node=ast.dump(elt)))
266+
continue
267+
return seq
268+
160269
class LogMessageHeader(object):
161270
def __init__(self, level, codePath, time, threadName=None, thread=None):
162271
self.level = level
@@ -303,13 +412,16 @@ def getSpeakIoMessage(self, mode):
303412
except Exception:
304413
log.error("Sequence cannot be spoken: {seq}".format(seq=match['seq']))
305414
return self.content
415+
# Supress Cancellable speech commands and callback speech commands before parsing the text because their
416+
# representation in the log is not valid Python syntax and in any case, we want to discard them.
306417
txtSeq = RE_CANCELLABLE_SPEECH.sub('', txtSeq)
307418
txtSeq = RE_CALLBACK_COMMAND.sub('', txtSeq)
308-
seq = eval(txtSeq)
309-
# Ignore CallbackCommand and ConfigProfileTriggerCommand to avoid producing errors or unexpected
310-
# side effects.
311-
if not preSpeechRefactor:
312-
seq = [c for c in seq if not isinstance(c, (CallbackCommand, ConfigProfileTriggerCommand))]
419+
try:
420+
seq = generateSpeechSequence(txtSeq)
421+
except SyntaxError: # When parsing the logged text of the sequence.
422+
log.error("Sequence cannot be spoken: {seq}".format(seq=match['seq']))
423+
# Fallback to full line
424+
return self.content
313425
if LogContainer.translateLog:
314426
seq2 = []
315427
for s in seq:

0 commit comments

Comments
 (0)