CVE-2020-27544 (published 2023-08-11T14:15Z)

An issue was discovered in FoldingAtHome Client Advanced Control GUI before commit 9b619ae64443997948a36dda01b420578de1af77, allows remote attackers to execute arbitrary code via crafted payload to function parse_message in file Connection.py.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/FoldingAtHome/fah-control
commit_details: repo_url: https://github.com/FoldingAtHome/fah-control
author: Joseph Coffland
author_date: 2020-09-21 22:37:40-07:00
msg: Use PYONDecoder instead of eval()
repository: repo_url: https://github.com/FoldingAtHome/fah-control
repo_name: FoldingAtHome/fah-control
description: Folding@home Client Advanced Control GUI
repo_language: Python
owner: FoldingAtHome
file_changes: filename: Connection.py
change_type: ModificationType.MODIFY
diff: @@ -27,8 +27,10 @@ import errno
 import time
 import sys
 import traceback
+import json
 
 from fah.util import OrderedDict
+from fah.util import PYONDecoder
 
 if sys.platform == 'win32':
     from ctypes import windll
@@ -204,7 +206,7 @@ class Connection:
 
     def parse_message(self, version, type, data):
         try:
-            msg = eval(data, {}, {})
+            msg = json.loads(data, cls = PYONDecoder)
             #if debug: print 'MSG:', type, msg
             self.messages.append((version, type, msg))
             self.last_message = time.time()

programming_language: Python
filename: PYONDecoder.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,216 @@
+################################################################################
+#                                                                              #
+#                    Folding@Home Client Control (FAHControl)                  #
+#                   Copyright (C) 2016-2020 foldingathome.org                  #
+#                  Copyright (C) 2010-2016 Stanford University                 #
+#                                                                              #
+#      This program is free software: you can redistribute it and/or modify    #
+#      it under the terms of the GNU General Public License as published by    #
+#       the Free Software Foundation, either version 3 of the License, or      #
+#                      (at your option) any later version.                     #
+#                                                                              #
+#        This program is distributed in the hope that it will be useful,       #
+#         but WITHOUT ANY WARRANTY; without even the implied warranty of       #
+#         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        #
+#                  GNU General Public License for more details.                #
+#                                                                              #
+#       You should have received a copy of the GNU General Public License      #
+#     along with this program.  If not, see <http://www.gnu.org/licenses/>.    #
+#                                                                              #
+################################################################################
+
+import re
+import json
+import sys
+
+
+NUMBER_RE = re.compile(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
+                       (re.VERBOSE | re.MULTILINE | re.DOTALL))
+FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
+STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS)
+BACKSLASH = {
+  '"': u'"', '\\': u'\\', '/': u'/',
+  'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t',
+}
+
+DEFAULT_ENCODING = "utf-8"
+
+
+def linecol(doc, pos):
+  lineno = doc.count('\n', 0, pos) + 1
+  if lineno == 1:
+    colno = pos + 1
+  else:
+    colno = pos - doc.rindex('\n', 0, pos)
+  return lineno, colno
+
+
+def errmsg(msg, doc, pos, end=None):
+  # Note that this function is called from _json
+  lineno, colno = linecol(doc, pos)
+  if end is None:
+    fmt = '{0}: line {1} column {2} (char {3})'
+    return fmt.format(msg, lineno, colno, pos)
+
+  endlineno, endcolno = linecol(doc, end)
+  fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})'
+  return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end)
+
+
+def _decode_uXXXX(s, pos):
+  esc = s[pos + 1:pos + 5]
+
+  if len(esc) == 4 and esc[1] not in 'xX':
+    try:
+      return int(esc, 16)
+    except ValueError: pass
+
+  msg = "Invalid \\uXXXX escape"
+  raise ValueError(errmsg(msg, s, pos))
+
+
+def pyon_scanstring(s, end, encoding = None, strict = True,
+                    _b = BACKSLASH, _m = STRINGCHUNK.match):
+  """Scan the string s for a JSON string. End is the index of the
+  character in s after the quote that started the JSON string.
+  Unescapes all valid JSON string escape sequences and raises ValueError
+  on attempt to decode an invalid string. If strict is False then literal
+  control characters are allowed in the string.
+  Returns a tuple of the decoded string and the index of the character in s
+  after the end quote."""
+  if encoding is None: encoding = DEFAULT_ENCODING
+  chunks = []
+  _append = chunks.append
+  begin = end - 1
+
+  while True:
+    chunk = _m(s, end)
+    if chunk is None:
+      raise ValueError(
+        errmsg("Unterminated string starting at", s, begin))
+
+    end = chunk.end()
+    content, terminator = chunk.groups()
+
+    # Content is contains zero or more unescaped string characters
+    if content:
+      if not isinstance(content, unicode):
+        content = unicode(content, encoding)
+      _append(content)
+
+    # Terminator is the end of string, a literal control character,
+    # or a backslash denoting that an escape sequence follows
+    if terminator == '"': break
+    elif terminator != '\\':
+      if strict:
+        msg = "Invalid control character {0!r} at".format(terminator)
+        raise ValueError(errmsg(msg, s, end))
+      else:
+        _append(terminator)
+        continue
+
+    try:
+      esc = s[end]
+    except IndexError:
+      raise ValueError(errmsg("Unterminated string starting at", s, begin))
+
+    # If not a unicode escape sequence, must be in the lookup table
+    if esc != 'u' and esc != 'x':
+      try:
+        char = _b[esc]
+      except KeyError:
+        msg = "Invalid \\escape: " + repr(esc)
+        raise ValueError(errmsg(msg, s, end))
+      end += 1
+
+    elif esc == 'x':
+      # Hex escape sequence
+      try:
+        code = s[end + 1: end + 3]
+        char = code.decode('hex')
+      except:
+        raise ValueError(errmsg('Invalid \\escape: ' + repr(code), s, end))
+
+      end += 3
+
+    else:
+      # Unicode escape sequence
+      uni = _decode_uXXXX(s, end)
+      end += 5
+      # Check for surrogate pair on UCS-4 systems
+      if sys.maxunicode > 65535 and \
+         0xd800 <= uni <= 0xdbff and s[end:end + 2] == '\\u':
+        uni2 = _decode_uXXXX(s, end + 1)
+        if 0xdc00 <= uni2 <= 0xdfff:
+          uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00))
+          end += 6
+      char = unichr(uni)
+
+    # Append the unescaped character
+    _append(char)
+
+  return u''.join(chunks), end
+
+
+
+def make_pyon_scanner(context):
+  parse_object = context.parse_object
+  parse_array = context.parse_array
+  parse_string = context.parse_string
+  match_number = NUMBER_RE.match
+  strict = context.strict
+  parse_float = context.parse_float
+  parse_int = context.parse_int
+  parse_constant = context.parse_constant
+  object_hook = context.object_hook
+  object_pairs_hook = context.object_pairs_hook
+
+
+  def scan_once(string, idx):
+    try:
+        nextchar = string[idx]
+    except IndexError:
+      raise StopIteration(idx)
+
+    if nextchar == '"': return parse_string(string, idx + 1, 'utf-8', strict)
+    elif nextchar == '{':
+      return parse_object((string, idx + 1), 'utf-8', strict, scan_once,
+                          object_hook, object_pairs_hook)
+    elif nextchar == '[':
+      return parse_array((string, idx + 1), scan_once)
+    elif nextchar == 'N' and string[idx:idx + 4] == 'None':
+      return None, idx + 4
+    elif nextchar == 'T' and string[idx:idx + 4] == 'True':
+      return True, idx + 4
+    elif nextchar == 'F' and string[idx:idx + 5] == 'False':
+      return False, idx + 5
+
+    m = match_number(string, idx)
+    if m is not None:
+      integer, frac, exp = m.groups()
+      if frac or exp:
+        res = parse_float(integer + (frac or '') + (exp or ''))
+      else: res = parse_int(integer)
+
+      return res, m.end()
+
+    elif nextchar == 'N' and string[idx:idx + 3] == 'NaN':
+      return parse_constant('NaN'), idx + 3
+
+    elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity':
+      return parse_constant('Infinity'), idx + 8
+
+    elif nextchar == '-' and string[idx:idx + 9] == '-Infinity':
+      return parse_constant('-Infinity'), idx + 9
+
+    else: raise StopIteration(idx)
+
+
+  return scan_once
+
+
+class PYONDecoder(json.JSONDecoder):
+  def __init__(self, *args, **kwargs):
+    json.JSONDecoder.__init__(self, *args, **kwargs)
+    self.parse_string = pyon_scanstring
+    self.scan_once = make_pyon_scanner(self)

programming_language: Python
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,7 @@ from SingleApp import *
 from EntryValidator import *
 from PasswordValidator import *
 from OrderedDict import *
+from PYONDecoder import *
 
 
 def parse_bool(x):

programming_language: Python